From 61f7401cb0e04710d53b49cf17fb70b9daf09e65 Mon Sep 17 00:00:00 2001 From: Ashunar0 Date: Mon, 19 Oct 2026 13:14:07 +0900 Subject: [PATCH 1/3] fix(handler): infer output type for defineHandler with middleware --- packages/void/src/runtime/handler.ts | 29 ++++++++++--------- .../void/test/unit/infer-props.typecheck.ts | 23 ++++++++++++++- 2 files changed, 38 insertions(+), 14 deletions(-) diff --git a/packages/void/src/runtime/handler.ts b/packages/void/src/runtime/handler.ts index eab24f2c0..17e2f0253 100644 --- a/packages/void/src/runtime/handler.ts +++ b/packages/void/src/runtime/handler.ts @@ -211,33 +211,36 @@ export type TypedHandler = { export function defineHandler( handler: (c: CloudContext) => R | Response | Promise, ): TypedHandler<{}, Exclude, Response>>; -export function defineHandler(firstMiddleware: MiddlewareFn, handler: HandlerFn): TypedHandler; -export function defineHandler( +export function defineHandler( + firstMiddleware: MiddlewareFn, + handler: (c: CloudContext) => R | Response | Promise, +): TypedHandler<{}, Exclude, Response>>; +export function defineHandler( firstMiddleware: MiddlewareFn, secondMiddleware: MiddlewareFn, - handler: HandlerFn, -): TypedHandler; -export function defineHandler( + handler: (c: CloudContext) => R | Response | Promise, +): TypedHandler<{}, Exclude, Response>>; +export function defineHandler( firstMiddleware: MiddlewareFn, secondMiddleware: MiddlewareFn, thirdMiddleware: MiddlewareFn, - handler: HandlerFn, -): TypedHandler; -export function defineHandler( + handler: (c: CloudContext) => R | Response | Promise, +): TypedHandler<{}, Exclude, Response>>; +export function defineHandler( firstMiddleware: MiddlewareFn, secondMiddleware: MiddlewareFn, thirdMiddleware: MiddlewareFn, fourthMiddleware: MiddlewareFn, - handler: HandlerFn, -): TypedHandler; -export function defineHandler( + handler: (c: CloudContext) => R | Response | Promise, +): TypedHandler<{}, Exclude, Response>>; +export function defineHandler( firstMiddleware: MiddlewareFn, secondMiddleware: MiddlewareFn, thirdMiddleware: MiddlewareFn, fourthMiddleware: MiddlewareFn, fifthMiddleware: MiddlewareFn, - handler: HandlerFn, -): TypedHandler; + handler: (c: CloudContext) => R | Response | Promise, +): TypedHandler<{}, Exclude, Response>>; export function defineHandler(...args: Array): TypedHandler { if (args.length === 1) { return args[0] as TypedHandler; diff --git a/packages/void/test/unit/infer-props.typecheck.ts b/packages/void/test/unit/infer-props.typecheck.ts index 7247e46ef..fd11b0c08 100644 --- a/packages/void/test/unit/infer-props.typecheck.ts +++ b/packages/void/test/unit/infer-props.typecheck.ts @@ -1,7 +1,7 @@ import { describe, expectTypeOf, it } from 'vite-plus/test'; import { z } from 'zod'; import type { InferProps } from '../../src/pages/defer.ts'; -import { defineHandler } from '../../src/runtime/handler.ts'; +import { defineHandler, defineMiddleware } from '../../src/runtime/handler.ts'; type Book = { id: string; title: string }; @@ -9,6 +9,10 @@ declare function findBook(id: string): Promise; const validators = { params: z.object({ id: z.string() }) }; +const middleware = defineMiddleware(async (_c, next) => { + await next(); +}); + describe('InferProps', () => { it('unwraps an async plain loader and excludes Response', () => { const loader = defineHandler(async (c) => { @@ -76,4 +80,21 @@ describe('InferProps', () => { book: Promise; }>(); }); + it('unwraps an async loader with middleware and excludes Response', () => { + const loader = defineHandler(middleware, async (c) => { + const book = await findBook('1'); + if (!book) { + return c.notFound(); + } + return { book }; + }); + + expectTypeOf>().toEqualTypeOf<{ book: Book }>(); + }); + + it('infers a loader with several middlewares', () => { + const loader = defineHandler(middleware, middleware, middleware, (_c) => ({ id: '1' })); + + expectTypeOf>().toEqualTypeOf<{ id: string }>(); + }); }); From 7eab0e61d445b197c4c7b2f13912158892499cb7 Mon Sep 17 00:00:00 2001 From: LongYinan Date: Mon, 19 Oct 2026 16:16:28 +0800 Subject: [PATCH 2/3] fix(handler): infer the whole return type of a defineHandler loader `(c) => R | Response | Promise` binds R from one promise, so a non-async loader that can return one of two promises, such as `cond ? loadAuthor() : loadBook()`, was rejected. The middleware forms accepted it before this branch. Take `(c) => R` and apply Awaited and Exclude to the output only, as withValidator does, in every overload. Co-Authored-By: Claude Opus 5.5 (1M context) --- packages/void/src/runtime/handler.ts | 12 ++++++------ packages/void/test/unit/infer-props.typecheck.ts | 16 ++++++++++++++++ 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/packages/void/src/runtime/handler.ts b/packages/void/src/runtime/handler.ts index 17e2f0253..e33e6c6f1 100644 --- a/packages/void/src/runtime/handler.ts +++ b/packages/void/src/runtime/handler.ts @@ -209,29 +209,29 @@ export type TypedHandler = { * Multiple arguments: middleware chain + final handler, composed at runtime. */ export function defineHandler( - handler: (c: CloudContext) => R | Response | Promise, + handler: (c: CloudContext) => R, ): TypedHandler<{}, Exclude, Response>>; export function defineHandler( firstMiddleware: MiddlewareFn, - handler: (c: CloudContext) => R | Response | Promise, + handler: (c: CloudContext) => R, ): TypedHandler<{}, Exclude, Response>>; export function defineHandler( firstMiddleware: MiddlewareFn, secondMiddleware: MiddlewareFn, - handler: (c: CloudContext) => R | Response | Promise, + handler: (c: CloudContext) => R, ): TypedHandler<{}, Exclude, Response>>; export function defineHandler( firstMiddleware: MiddlewareFn, secondMiddleware: MiddlewareFn, thirdMiddleware: MiddlewareFn, - handler: (c: CloudContext) => R | Response | Promise, + handler: (c: CloudContext) => R, ): TypedHandler<{}, Exclude, Response>>; export function defineHandler( firstMiddleware: MiddlewareFn, secondMiddleware: MiddlewareFn, thirdMiddleware: MiddlewareFn, fourthMiddleware: MiddlewareFn, - handler: (c: CloudContext) => R | Response | Promise, + handler: (c: CloudContext) => R, ): TypedHandler<{}, Exclude, Response>>; export function defineHandler( firstMiddleware: MiddlewareFn, @@ -239,7 +239,7 @@ export function defineHandler( thirdMiddleware: MiddlewareFn, fourthMiddleware: MiddlewareFn, fifthMiddleware: MiddlewareFn, - handler: (c: CloudContext) => R | Response | Promise, + handler: (c: CloudContext) => R, ): TypedHandler<{}, Exclude, Response>>; export function defineHandler(...args: Array): TypedHandler { if (args.length === 1) { diff --git a/packages/void/test/unit/infer-props.typecheck.ts b/packages/void/test/unit/infer-props.typecheck.ts index fd11b0c08..a9b4df136 100644 --- a/packages/void/test/unit/infer-props.typecheck.ts +++ b/packages/void/test/unit/infer-props.typecheck.ts @@ -4,8 +4,11 @@ import type { InferProps } from '../../src/pages/defer.ts'; import { defineHandler, defineMiddleware } from '../../src/runtime/handler.ts'; type Book = { id: string; title: string }; +type Author = { id: string; name: string }; declare function findBook(id: string): Promise; +declare function findAuthor(id: string): Promise; +declare const showAuthor: boolean; const validators = { params: z.object({ id: z.string() }) }; @@ -97,4 +100,17 @@ describe('InferProps', () => { expectTypeOf>().toEqualTypeOf<{ id: string }>(); }); + + it('keeps every promise a non-async loader can return', () => { + const load = () => + showAuthor + ? findAuthor('1').then((author) => ({ author })) + : findBook('1').then((book) => ({ book })); + const loader = defineHandler((_c) => load()); + const guarded = defineHandler(middleware, (_c) => load()); + + type Expected = { author: Author } | { book: Book | undefined }; + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + }); }); From 5d24c3cef6881bb1f5de065a6c76fdbc672c35cc Mon Sep 17 00:00:00 2001 From: LongYinan Date: Mon, 19 Oct 2026 16:21:56 +0800 Subject: [PATCH 3/3] fix(handler): keep explicit defineHandler props types The previous commit typed the handler as `(c) => R`, which broke the documented `defineHandler(async (c) => ...)`: R is the loader's data, not its return value (platform dashboard typecheck failed). Keep `(c) => R | Response | Promise` for every overload, as on main, and give each middleware form a second overload that infers from the handler's return type, for loaders like `cond ? loadA() : loadB()`. The single-handler overload is unchanged from main. Co-Authored-By: Claude Opus 5.5 (1M context) --- packages/void/src/runtime/handler.ts | 46 ++++++++++++++++--- .../void/test/unit/infer-props.typecheck.ts | 26 +++++++---- 2 files changed, 58 insertions(+), 14 deletions(-) diff --git a/packages/void/src/runtime/handler.ts b/packages/void/src/runtime/handler.ts index e33e6c6f1..9804e091b 100644 --- a/packages/void/src/runtime/handler.ts +++ b/packages/void/src/runtime/handler.ts @@ -126,6 +126,10 @@ declare module 'hono' { export type CloudContext = Context; type HandlerFn = (c: CloudContext) => unknown | Promise; +// `R` is the loader's data, so `defineHandler(async (c) => ...)` works. +type LoaderFn = (c: CloudContext) => R | Response | Promise; +// For loaders that LoaderFn cannot infer, such as `cond ? loadA() : loadB()`. +type LoaderOutput = Exclude>, Response>; type MiddlewareFn = MiddlewareHandler; type BasicAuthOptionValue = string | (() => string); @@ -209,38 +213,68 @@ export type TypedHandler = { * Multiple arguments: middleware chain + final handler, composed at runtime. */ export function defineHandler( - handler: (c: CloudContext) => R, + handler: LoaderFn, ): TypedHandler<{}, Exclude, Response>>; export function defineHandler( firstMiddleware: MiddlewareFn, - handler: (c: CloudContext) => R, + handler: LoaderFn, ): TypedHandler<{}, Exclude, Response>>; +export function defineHandler( + firstMiddleware: MiddlewareFn, + handler: F, +): TypedHandler<{}, LoaderOutput>; export function defineHandler( firstMiddleware: MiddlewareFn, secondMiddleware: MiddlewareFn, - handler: (c: CloudContext) => R, + handler: LoaderFn, ): TypedHandler<{}, Exclude, Response>>; +export function defineHandler( + firstMiddleware: MiddlewareFn, + secondMiddleware: MiddlewareFn, + handler: F, +): TypedHandler<{}, LoaderOutput>; export function defineHandler( firstMiddleware: MiddlewareFn, secondMiddleware: MiddlewareFn, thirdMiddleware: MiddlewareFn, - handler: (c: CloudContext) => R, + handler: LoaderFn, ): TypedHandler<{}, Exclude, Response>>; +export function defineHandler( + firstMiddleware: MiddlewareFn, + secondMiddleware: MiddlewareFn, + thirdMiddleware: MiddlewareFn, + handler: F, +): TypedHandler<{}, LoaderOutput>; export function defineHandler( firstMiddleware: MiddlewareFn, secondMiddleware: MiddlewareFn, thirdMiddleware: MiddlewareFn, fourthMiddleware: MiddlewareFn, - handler: (c: CloudContext) => R, + handler: LoaderFn, ): TypedHandler<{}, Exclude, Response>>; +export function defineHandler( + firstMiddleware: MiddlewareFn, + secondMiddleware: MiddlewareFn, + thirdMiddleware: MiddlewareFn, + fourthMiddleware: MiddlewareFn, + handler: F, +): TypedHandler<{}, LoaderOutput>; export function defineHandler( firstMiddleware: MiddlewareFn, secondMiddleware: MiddlewareFn, thirdMiddleware: MiddlewareFn, fourthMiddleware: MiddlewareFn, fifthMiddleware: MiddlewareFn, - handler: (c: CloudContext) => R, + handler: LoaderFn, ): TypedHandler<{}, Exclude, Response>>; +export function defineHandler( + firstMiddleware: MiddlewareFn, + secondMiddleware: MiddlewareFn, + thirdMiddleware: MiddlewareFn, + fourthMiddleware: MiddlewareFn, + fifthMiddleware: MiddlewareFn, + handler: F, +): TypedHandler<{}, LoaderOutput>; export function defineHandler(...args: Array): TypedHandler { if (args.length === 1) { return args[0] as TypedHandler; diff --git a/packages/void/test/unit/infer-props.typecheck.ts b/packages/void/test/unit/infer-props.typecheck.ts index a9b4df136..5aa263195 100644 --- a/packages/void/test/unit/infer-props.typecheck.ts +++ b/packages/void/test/unit/infer-props.typecheck.ts @@ -101,16 +101,26 @@ describe('InferProps', () => { expectTypeOf>().toEqualTypeOf<{ id: string }>(); }); - it('keeps every promise a non-async loader can return', () => { - const load = () => + it('keeps every promise a non-async loader with middleware can return', () => { + const loader = defineHandler(middleware, (_c) => showAuthor ? findAuthor('1').then((author) => ({ author })) - : findBook('1').then((book) => ({ book })); - const loader = defineHandler((_c) => load()); - const guarded = defineHandler(middleware, (_c) => load()); + : findBook('1').then((book) => ({ book })), + ); - type Expected = { author: Author } | { book: Book | undefined }; - expectTypeOf>().toEqualTypeOf(); - expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf< + { author: Author } | { book: Book | undefined } + >(); + }); + + it('takes the props type as an explicit type argument', () => { + type Props = { title: string }; + const loader = defineHandler(async () => ({ title: 'a' })); + const guarded = defineHandler(middleware, async (c) => + c.req.query('missing') ? c.notFound() : { title: 'a' }, + ); + + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); }); });