Skip to content

fix(types): resolve RPC type inference for arrow functions returning Promise chains - #5251

Closed
yigiterturk-dev wants to merge 1 commit into
honojs:mainfrom
yigiterturk-dev:fix/rpc-arrow-promise-types
Closed

fix(types): resolve RPC type inference for arrow functions returning Promise chains#5251
yigiterturk-dev wants to merge 1 commit into
honojs:mainfrom
yigiterturk-dev:fix/rpc-arrow-promise-types

Conversation

@yigiterturk-dev

Copy link
Copy Markdown
Contributor

Summary

Fix MergeTypedResponse to correctly infer TypedResponse output types when handlers return Promise chains (e.g., Promise.resolve(...).then((d) => c.json(d))).

Closes #4765

Problem

When using arrow functions that return a Promise chain directly, the RPC client infers unknown as the output type:

const app = new Hono().get("/", (c) =>
  Promise.resolve({ hello: "world" }).then((d) => c.json(d))
);

type AppType = typeof app;
const client = hc<AppType>("http://localhost");
const res = await client.index.$get();
const data = await res.json(); // ❌ Type is unknown

Root Cause

TypeScript infers the handler return type as Promise<Response & TypedResponse<{hello: string}, 200, "json">>. The previous MergeTypedResponse implementation only unwrapped one level of Promise, so when the inner type was still wrapped in a Promise (nested Promise chains), it fell through to the default TypedResponse (with no type parameter), resulting in unknown on the client side.

Solution

Added recursive Promise unwrapping to MergeTypedResponse:

 type MergeTypedResponse<T> =
-  T extends Promise<void>
-    ? T
-    : T extends Promise<infer T2>
-      ? T2 extends TypedResponse
-        ? T2
-        : TypedResponse
-      : T extends TypedResponse
-        ? T
+  T extends Promise<infer T2>
+    ? T2 extends TypedResponse
+      ? T2
+      : T2 extends Promise<any>
+        ? MergeTypedResponse<T2>
         : TypedResponse
+    : T extends TypedResponse
+      ? T
+      : TypedResponse

Key changes:

  1. Recursive Promise unwrapping — If the inner type of a Promise is itself a Promise, recursively unwrap until we find a TypedResponse or fall through to the default.
  2. Removed special Promise<void> case — The existing fallback to TypedResponse already handles this correctly; the special case was redundant.

Testing

Fix MergeTypedResponse to recursively unwrap nested Promise types,
allowing arrow functions that return Promise chains (e.g.,
Promise.resolve(...).then((d) => c.json(d))) to correctly infer
TypedResponse output types in the RPC client.

Previously, handlers returning Promise<Response & TypedResponse<T>>
would result in 'unknown' output types on the client side because
MergeTypedResponse did not handle nested Promise unwrapping.

The fix adds recursive Promise unwrapping to MergeTypedResponse while
preserving all existing type behavior.

Closes #4765
@yigiterturk-dev

Copy link
Copy Markdown
Contributor Author

Closing this in favour of documenting the limitation.

While working on the fix I found that the response type is not lost in MergeTypedResponse: the handler returns Promise<JSONRespondReturn<{ hello: string }>>, and that already satisfies TypedResponse. What happens instead is that R never gets inferred from the handler at all. Inside the then() callback the type of c depends on the handler's return type, which is what the callback is being used to infer, so TypeScript falls back to the constraint HandlerResponse<any>. The conditional type then distributes over that union and the route schema ends up as a union that includes Promise<void>, which is why the client sees unknown.

So this is the case @yusukebe described in #4765 — fixing it in the type definitions is not a small change. Following the suggestion there to document it instead: honojs/website#897.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

BUG: RPC types broken when using arrow function with a Promise

1 participant