diff --git a/packages/void/src/runtime/handler.ts b/packages/void/src/runtime/handler.ts index eab24f2c0..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,35 +213,68 @@ export type TypedHandler = { * Multiple arguments: middleware chain + final handler, composed at runtime. */ export function defineHandler( - handler: (c: CloudContext) => R | Response | Promise, + handler: LoaderFn, ): TypedHandler<{}, Exclude, Response>>; -export function defineHandler(firstMiddleware: MiddlewareFn, handler: HandlerFn): TypedHandler; -export function defineHandler( +export function defineHandler( + firstMiddleware: MiddlewareFn, + handler: LoaderFn, +): TypedHandler<{}, Exclude, Response>>; +export function defineHandler( + firstMiddleware: MiddlewareFn, + handler: F, +): TypedHandler<{}, LoaderOutput>; +export function defineHandler( + firstMiddleware: MiddlewareFn, + secondMiddleware: MiddlewareFn, + 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: LoaderFn, +): TypedHandler<{}, Exclude, Response>>; +export function defineHandler( firstMiddleware: MiddlewareFn, secondMiddleware: MiddlewareFn, - handler: HandlerFn, -): TypedHandler; -export function defineHandler( + thirdMiddleware: MiddlewareFn, + handler: F, +): TypedHandler<{}, LoaderOutput>; +export function defineHandler( firstMiddleware: MiddlewareFn, secondMiddleware: MiddlewareFn, thirdMiddleware: MiddlewareFn, - handler: HandlerFn, -): TypedHandler; -export function defineHandler( + fourthMiddleware: MiddlewareFn, + handler: LoaderFn, +): TypedHandler<{}, Exclude, Response>>; +export function defineHandler( firstMiddleware: MiddlewareFn, secondMiddleware: MiddlewareFn, thirdMiddleware: MiddlewareFn, fourthMiddleware: MiddlewareFn, - handler: HandlerFn, -): TypedHandler; -export function defineHandler( + handler: F, +): TypedHandler<{}, LoaderOutput>; +export function defineHandler( + firstMiddleware: MiddlewareFn, + secondMiddleware: MiddlewareFn, + thirdMiddleware: MiddlewareFn, + fourthMiddleware: MiddlewareFn, + fifthMiddleware: MiddlewareFn, + handler: LoaderFn, +): TypedHandler<{}, Exclude, Response>>; +export function defineHandler( firstMiddleware: MiddlewareFn, secondMiddleware: MiddlewareFn, thirdMiddleware: MiddlewareFn, fourthMiddleware: MiddlewareFn, fifthMiddleware: MiddlewareFn, - handler: HandlerFn, -): TypedHandler; + 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 7247e46ef..5aa263195 100644 --- a/packages/void/test/unit/infer-props.typecheck.ts +++ b/packages/void/test/unit/infer-props.typecheck.ts @@ -1,14 +1,21 @@ 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 }; +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() }) }; +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 +83,44 @@ 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 }>(); + }); + + 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 })), + ); + + 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(); + }); });