From f55f3cb400a21f70fe04fa061611f8839aaff073 Mon Sep 17 00:00:00 2001 From: Lucas Date: Sat, 11 Oct 2025 06:59:55 +0200 Subject: [PATCH 01/16] refactor(types): make `params` generic in `H3EventContext` * Updated the `H3EventContext` interface to accept a generic type parameter `TParams` for `params`. * This change enhances type safety and flexibility for router parameter handling. --- src/types/context.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/types/context.ts b/src/types/context.ts index 1f4f09e5e..78c8a9b80 100644 --- a/src/types/context.ts +++ b/src/types/context.ts @@ -2,9 +2,10 @@ import type { Session } from "../utils/session.ts"; import type { H3Route } from "./h3.ts"; import type { ServerRequestContext } from "srvx"; -export interface H3EventContext extends ServerRequestContext { +export interface H3EventContext> + extends ServerRequestContext { /* Matched router parameters */ - params?: Record; + params?: TParams; /* Matched middleware parameters */ middlewareParams?: Record; From d2f51dfb987b1ff97ee7db238f592aa446910b13 Mon Sep 17 00:00:00 2001 From: Lucas Date: Sat, 11 Oct 2025 07:00:57 +0200 Subject: [PATCH 02/16] refactor(types): enhance `H3Event` context to infer router parameters * Updated the `context` property in `H3Event` to infer `routerParams` more effectively. * Added tests to verify the inference of router parameters from `EventHandlerRequest`. * Ensured default behavior for cases without specified `routerParams`. --- src/event.ts | 7 ++++++- test/unit/types.test-d.ts | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/src/event.ts b/src/event.ts index 004ecb7a0..a4af97e06 100644 --- a/src/event.ts +++ b/src/event.ts @@ -56,7 +56,12 @@ export class H3Event< /** * Event context. */ - readonly context: H3EventContext; + readonly context: _RequestT["routerParams"] extends infer P extends Record< + string, + string + > + ? Omit, "params"> & { params: P } + : H3EventContext>; /** * @internal diff --git a/test/unit/types.test-d.ts b/test/unit/types.test-d.ts index adbac59eb..f72f2cbde 100644 --- a/test/unit/types.test-d.ts +++ b/test/unit/types.test-d.ts @@ -124,4 +124,38 @@ describe("types", () => { }); }); }); + + describe("routerParams inference", () => { + it("should infer router params from EventHandlerRequest (non-optional)", () => { + defineHandler<{ + routerParams: { id: string; name: string }; + }>((event) => { + expectTypeOf(event.context.params).toEqualTypeOf<{ + id: string; + name: string; + }>(); + expectTypeOf(event.context.params.id).toEqualTypeOf(); + expectTypeOf(event.context.params.name).toEqualTypeOf(); + }); + }); + + it("should default to optional Record when no routerParams specified", () => { + defineHandler((event) => { + expectTypeOf(event.context.params).toEqualTypeOf< + Record | undefined + >(); + }); + }); + + it("should work with specific param types (non-optional)", () => { + defineHandler<{ + routerParams: { userId: string; postId: string }; + }>((event) => { + const userId = event.context.params.userId; + const postId = event.context.params.postId; + expectTypeOf(userId).toEqualTypeOf(); + expectTypeOf(postId).toEqualTypeOf(); + }); + }); + }); }); From adaf236645d2456c3ae0ccfe1a93c9d1b6b3fa8b Mon Sep 17 00:00:00 2001 From: Lucas Date: Sat, 11 Oct 2025 07:01:48 +0200 Subject: [PATCH 03/16] refactor: make `getRouterParams` and `getRouterParam` work with typed params --- src/utils/request.ts | 35 ++++++++++++++++++++++++----------- test/unit/types.test-d.ts | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 11 deletions(-) diff --git a/src/utils/request.ts b/src/utils/request.ts index 949872a4f..fa4855a7e 100644 --- a/src/utils/request.ts +++ b/src/utils/request.ts @@ -115,15 +115,17 @@ export function getValidatedQuery( * const params = getRouterParams(event); // { key: "value" } * }); */ -export function getRouterParams( - event: HTTPEvent, +export function getRouterParams( + event: Event, opts: { decode?: boolean } = {}, -): NonNullable { +): Event extends H3Event + ? R["routerParams"] extends infer P extends Record + ? P + : Record + : Record { // Fallback object needs to be returned in case router is not used (#149) const context = getEventContext(event); - let params = (context.params || {}) as NonNullable< - H3Event["context"]["params"] - >; + let params = (context.params || {}) as any; if (opts.decode) { params = { ...params }; for (const key in params) { @@ -196,13 +198,24 @@ export function getValidatedRouterParams( * const param = getRouterParam(event, "key"); * }); */ -export function getRouterParam( - event: HTTPEvent, - name: string, +export function getRouterParam< + Event extends HTTPEvent, + Key extends Event extends H3Event + ? R["routerParams"] extends infer P extends Record + ? keyof P & string + : string + : string, +>( + event: Event, + name: Key, opts: { decode?: boolean } = {}, -): string | undefined { +): Event extends H3Event + ? R["routerParams"] extends infer P extends Record + ? P[Key] + : string | undefined + : string | undefined { const params = getRouterParams(event, opts); - return params[name]; + return params[name] as any; } /** diff --git a/test/unit/types.test-d.ts b/test/unit/types.test-d.ts index f72f2cbde..452db5abb 100644 --- a/test/unit/types.test-d.ts +++ b/test/unit/types.test-d.ts @@ -8,6 +8,8 @@ import { readValidatedBody, getValidatedQuery, defineValidatedHandler, + getRouterParams, + getRouterParam, } from "../../src/index.ts"; import { z } from "zod"; @@ -157,5 +159,37 @@ describe("types", () => { expectTypeOf(postId).toEqualTypeOf(); }); }); + + it("should work with getRouterParams helper", () => { + defineHandler<{ + routerParams: { id: string; slug: string }; + }>((event) => { + const params = getRouterParams(event); + expectTypeOf(params).toEqualTypeOf<{ id: string; slug: string }>(); + expectTypeOf(params.id).toEqualTypeOf(); + expectTypeOf(params.slug).toEqualTypeOf(); + }); + }); + + it("should work with getRouterParam helper", () => { + defineHandler<{ + routerParams: { id: string; slug: string }; + }>((event) => { + const id = getRouterParam(event, "id"); + const slug = getRouterParam(event, "slug"); + expectTypeOf(id).toEqualTypeOf(); + expectTypeOf(slug).toEqualTypeOf(); + }); + }); + + it("getRouterParam should provide autocomplete for param keys", () => { + defineHandler<{ + routerParams: { userId: string; postId: string }; + }>((event) => { + // This should only allow "userId" | "postId" as the second parameter + const userId = getRouterParam(event, "userId"); + expectTypeOf(userId).toEqualTypeOf(); + }); + }); }); }); From 6ccaeb28fbcdbd4fb95ad16a0b08ab44afeb74db Mon Sep 17 00:00:00 2001 From: Lucas Date: Sat, 11 Oct 2025 07:34:40 +0200 Subject: [PATCH 04/16] refactor(types): simplify `H3Event` context type definition --- src/event.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/event.ts b/src/event.ts index a4af97e06..9480c99be 100644 --- a/src/event.ts +++ b/src/event.ts @@ -56,12 +56,7 @@ export class H3Event< /** * Event context. */ - readonly context: _RequestT["routerParams"] extends infer P extends Record< - string, - string - > - ? Omit, "params"> & { params: P } - : H3EventContext>; + readonly context: H3EventContext<_RequestT["routerParams"]>; /** * @internal From 8f7ead7014a558c63e7723448d4d4ef11ce244ae Mon Sep 17 00:00:00 2001 From: Lucas Date: Sat, 11 Oct 2025 07:35:40 +0200 Subject: [PATCH 05/16] refactor(types): make provided params required in context This feels cleaner to have the optionality of the params in the context. --- src/types/context.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/types/context.ts b/src/types/context.ts index 78c8a9b80..8c4c0d85f 100644 --- a/src/types/context.ts +++ b/src/types/context.ts @@ -5,7 +5,9 @@ import type { ServerRequestContext } from "srvx"; export interface H3EventContext> extends ServerRequestContext { /* Matched router parameters */ - params?: TParams; + params: Record extends TParams + ? TParams | undefined + : TParams; /* Matched middleware parameters */ middlewareParams?: Record; From df68165b1693d9d67343af7db74899d8bd28a2b2 Mon Sep 17 00:00:00 2001 From: Lucas Date: Sat, 11 Oct 2025 07:43:20 +0200 Subject: [PATCH 06/16] refactor(types): simplify types for router parameters inference --- src/event.ts | 5 ++++- src/types/context.ts | 4 +--- src/utils/request.ts | 25 ++++++++++++------------- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/event.ts b/src/event.ts index 9480c99be..1a0e7b7ac 100644 --- a/src/event.ts +++ b/src/event.ts @@ -56,7 +56,10 @@ export class H3Event< /** * Event context. */ - readonly context: H3EventContext<_RequestT["routerParams"]>; + readonly context: _RequestT["routerParams"] extends infer _RouteParams extends + Record + ? Omit, "params"> & { params: _RouteParams } + : H3EventContext; /** * @internal diff --git a/src/types/context.ts b/src/types/context.ts index 8c4c0d85f..78c8a9b80 100644 --- a/src/types/context.ts +++ b/src/types/context.ts @@ -5,9 +5,7 @@ import type { ServerRequestContext } from "srvx"; export interface H3EventContext> extends ServerRequestContext { /* Matched router parameters */ - params: Record extends TParams - ? TParams | undefined - : TParams; + params?: TParams; /* Matched middleware parameters */ middlewareParams?: Record; diff --git a/src/utils/request.ts b/src/utils/request.ts index fa4855a7e..9e670450c 100644 --- a/src/utils/request.ts +++ b/src/utils/request.ts @@ -118,21 +118,21 @@ export function getValidatedQuery( export function getRouterParams( event: Event, opts: { decode?: boolean } = {}, -): Event extends H3Event - ? R["routerParams"] extends infer P extends Record - ? P - : Record - : Record { +): Event extends H3Event ? R["routerParams"] : Record { // Fallback object needs to be returned in case router is not used (#149) const context = getEventContext(event); - let params = (context.params || {}) as any; + let params = (context.params || {}) as NonNullable< + H3Event["context"]["params"] + >; + if (opts.decode) { params = { ...params }; for (const key in params) { params[key] = decodeURIComponent(params[key]); } } - return params; + + return params as any; } export function getValidatedRouterParams< @@ -201,21 +201,20 @@ export function getValidatedRouterParams( export function getRouterParam< Event extends HTTPEvent, Key extends Event extends H3Event - ? R["routerParams"] extends infer P extends Record - ? keyof P & string - : string + ? keyof R["routerParams"] & string : string, >( event: Event, name: Key, opts: { decode?: boolean } = {}, ): Event extends H3Event - ? R["routerParams"] extends infer P extends Record - ? P[Key] + ? R["routerParams"] extends Record + ? R["routerParams"][Key] : string | undefined : string | undefined { const params = getRouterParams(event, opts); - return params[name] as any; + + return params?.[name] as any; } /** From 385ff92b2f4b29e4bf0ac0638c039ce2917d5d62 Mon Sep 17 00:00:00 2001 From: Lucas Date: Sat, 11 Oct 2025 07:58:02 +0200 Subject: [PATCH 07/16] refactor(types): use overloads for `getRouterParams` and `getRouterParam` --- src/utils/request.ts | 32 ++++++++++++++++++++++---------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/src/utils/request.ts b/src/utils/request.ts index 9e670450c..a80ede553 100644 --- a/src/utils/request.ts +++ b/src/utils/request.ts @@ -115,10 +115,18 @@ export function getValidatedQuery( * const params = getRouterParams(event); // { key: "value" } * }); */ +export function getRouterParams( + event: Event, + opts?: { decode?: boolean }, +): Event extends H3Event ? R["routerParams"] : never; +export function getRouterParams( + event: Event, + opts?: { decode?: boolean }, +): Record; export function getRouterParams( event: Event, opts: { decode?: boolean } = {}, -): Event extends H3Event ? R["routerParams"] : Record { +): Record { // Fallback object needs to be returned in case router is not used (#149) const context = getEventContext(event); let params = (context.params || {}) as NonNullable< @@ -199,19 +207,23 @@ export function getValidatedRouterParams( * }); */ export function getRouterParam< - Event extends HTTPEvent, - Key extends Event extends H3Event - ? keyof R["routerParams"] & string - : string, + Event extends H3Event, + Key extends Event extends H3Event ? keyof R["routerParams"] & string : never, >( event: Event, name: Key, + opts?: { decode?: boolean }, +): Event extends H3Event ? R["routerParams"][Key] : never; +export function getRouterParam( + event: Event, + name: string, + opts?: { decode?: boolean }, +): string | undefined; +export function getRouterParam( + event: Event, + name: string, opts: { decode?: boolean } = {}, -): Event extends H3Event - ? R["routerParams"] extends Record - ? R["routerParams"][Key] - : string | undefined - : string | undefined { +): string | undefined { const params = getRouterParams(event, opts); return params?.[name] as any; From e455e6c73fb68b6059b3d21205933bb5d53422d7 Mon Sep 17 00:00:00 2001 From: Lucas Date: Sat, 11 Oct 2025 08:00:28 +0200 Subject: [PATCH 08/16] refactor(types): remove use of any in `getRouterParams` and `getRouterParam` --- src/utils/request.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/utils/request.ts b/src/utils/request.ts index a80ede553..e2f3b96f1 100644 --- a/src/utils/request.ts +++ b/src/utils/request.ts @@ -122,11 +122,11 @@ export function getRouterParams( export function getRouterParams( event: Event, opts?: { decode?: boolean }, -): Record; +): NonNullable; export function getRouterParams( event: Event, opts: { decode?: boolean } = {}, -): Record { +): NonNullable { // Fallback object needs to be returned in case router is not used (#149) const context = getEventContext(event); let params = (context.params || {}) as NonNullable< @@ -140,7 +140,7 @@ export function getRouterParams( } } - return params as any; + return params; } export function getValidatedRouterParams< @@ -226,7 +226,7 @@ export function getRouterParam( ): string | undefined { const params = getRouterParams(event, opts); - return params?.[name] as any; + return params[name]; } /** From 9f4b1caefa8bf371018467761f22dbef7cc86de7 Mon Sep 17 00:00:00 2001 From: Lucas Date: Sat, 11 Oct 2025 08:02:00 +0200 Subject: [PATCH 09/16] refactor(types): reorganize `getRouterParams` and `getRouterParam` function signatures --- src/utils/request.ts | 40 +++++++++++++++++++++------------------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/src/utils/request.ts b/src/utils/request.ts index e2f3b96f1..650380440 100644 --- a/src/utils/request.ts +++ b/src/utils/request.ts @@ -105,6 +105,14 @@ export function getValidatedQuery( return validateData(query, validate); } +export function getRouterParams( + event: Event, + opts?: { decode?: boolean }, +): Event extends H3Event ? R["routerParams"] : never; +export function getRouterParams( + event: Event, + opts?: { decode?: boolean }, +): NonNullable; /** * Get matched route params. * @@ -115,14 +123,6 @@ export function getValidatedQuery( * const params = getRouterParams(event); // { key: "value" } * }); */ -export function getRouterParams( - event: Event, - opts?: { decode?: boolean }, -): Event extends H3Event ? R["routerParams"] : never; -export function getRouterParams( - event: Event, - opts?: { decode?: boolean }, -): NonNullable; export function getRouterParams( event: Event, opts: { decode?: boolean } = {}, @@ -196,19 +196,11 @@ export function getValidatedRouterParams( return validateData(routerParams, validate); } -/** - * Get a matched route param by name. - * - * If `decode` option is `true`, it will decode the matched route param using `decodeURI`. - * - * @example - * app.get("/", (event) => { - * const param = getRouterParam(event, "key"); - * }); - */ export function getRouterParam< Event extends H3Event, - Key extends Event extends H3Event ? keyof R["routerParams"] & string : never, + Key extends Event extends H3Event + ? keyof R["routerParams"] & string + : never, >( event: Event, name: Key, @@ -219,6 +211,16 @@ export function getRouterParam( name: string, opts?: { decode?: boolean }, ): string | undefined; +/** + * Get a matched route param by name. + * + * If `decode` option is `true`, it will decode the matched route param using `decodeURI`. + * + * @example + * app.get("/", (event) => { + * const param = getRouterParam(event, "key"); + * }); + */ export function getRouterParam( event: Event, name: string, From 52667afc47185145cfbd39e194a38c48db846d25 Mon Sep 17 00:00:00 2001 From: Lucas Date: Sun, 12 Oct 2025 05:59:12 +0200 Subject: [PATCH 10/16] refactor(types): enhance `H3Event` context type definition The previous implementation in the pull request, removed access to the optional properties. --- src/event.ts | 9 +++++---- src/types/context.ts | 6 +++++- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/event.ts b/src/event.ts index 1a0e7b7ac..6d9d40df7 100644 --- a/src/event.ts +++ b/src/event.ts @@ -56,10 +56,11 @@ export class H3Event< /** * Event context. */ - readonly context: _RequestT["routerParams"] extends infer _RouteParams extends - Record - ? Omit, "params"> & { params: _RouteParams } - : H3EventContext; + readonly context: H3EventContext<_RequestT["routerParams"]> & { + params: _RequestT["routerParams"] extends undefined + ? undefined + : _RequestT["routerParams"]; + }; /** * @internal diff --git a/src/types/context.ts b/src/types/context.ts index 78c8a9b80..6d70af821 100644 --- a/src/types/context.ts +++ b/src/types/context.ts @@ -4,7 +4,11 @@ import type { ServerRequestContext } from "srvx"; export interface H3EventContext> extends ServerRequestContext { - /* Matched router parameters */ + /** + * Matched route parameters + * + * If there are no parameters, this will be `undefined`. + */ params?: TParams; /* Matched middleware parameters */ From f7c9ececc828028b8325a7e94a10479c90465133 Mon Sep 17 00:00:00 2001 From: Lucas Date: Sun, 12 Oct 2025 05:59:40 +0200 Subject: [PATCH 11/16] feat(types): enhance route parameter handling in HTTP methods * Added `RouteParams` type to simplify route parameter inference. * Updated `on`, `get`, `post`, `put`, `delete`, `patch`, `head`, `options`, `connect`, and `trace` methods to utilize the new `RouteParams` type for better type safety. * Improved type definitions for event handlers to include inferred route parameters. --- src/h3.ts | 23 +++++++++++-- src/types/_utils.ts | 8 +++++ src/types/h3.ts | 83 +++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 110 insertions(+), 4 deletions(-) diff --git a/src/h3.ts b/src/h3.ts index cc0ebe0a6..6b8b4ad2f 100644 --- a/src/h3.ts +++ b/src/h3.ts @@ -4,7 +4,7 @@ import { toResponse, kNotFound } from "./response.ts"; import { callMiddleware, normalizeMiddleware } from "./middleware.ts"; import type { ServerRequest } from "srvx"; -import type { RouterContext, MatchedRoute } from "rou3"; +import type { RouterContext, MatchedRoute, InferRouteParams } from "rou3"; import type { H3Config, H3CoreConfig, H3Plugin } from "./types/h3.ts"; import type { H3EventContext } from "./types/context.ts"; import type { @@ -25,6 +25,7 @@ import type { import { toRequest } from "./utils/request.ts"; import { toEventHandler } from "./handler.ts"; +import type { RouteParams } from "./types/_utils.ts"; export const NoHandler: EventHandler = () => kNotFound; @@ -158,18 +159,36 @@ export const H3 = /* @__PURE__ */ (() => { return this; } + on( + method: HTTPMethod | Lowercase | "", + route: Route, + handler: EventHandler<{ + routerParams: RouteParams>; + }>, + opts?: RouteOptions, + ): this; on( method: HTTPMethod | Lowercase | "", route: string, handler: HTTPHandler, opts?: RouteOptions, + ): this; + on( + method: HTTPMethod | Lowercase | "", + route: Route | string, + handler: + | EventHandler<{ + routerParams: RouteParams>; + }> + | HTTPHandler, + opts?: RouteOptions, ): this { const _method = (method || "").toUpperCase(); route = new URL(route, "http://_").pathname; this._addRoute({ method: _method as HTTPMethod, route, - handler: toEventHandler(handler)!, + handler: toEventHandler(handler as HTTPHandler)!, middleware: opts?.middleware, meta: { ...(handler as EventHandler).meta, ...opts?.meta }, }); diff --git a/src/types/_utils.ts b/src/types/_utils.ts index 4773ec36c..84c64cab0 100644 --- a/src/types/_utils.ts +++ b/src/types/_utils.ts @@ -1 +1,9 @@ export type MaybePromise = T | Promise; + +export type Simplify = { [K in keyof T]: T[K] } & {}; +export type RouteParams = + Simplify extends infer _Simplified + ? keyof _Simplified extends never + ? undefined + : _Simplified + : never; diff --git a/src/types/h3.ts b/src/types/h3.ts index 9c261aaf5..94cbeb58b 100644 --- a/src/types/h3.ts +++ b/src/types/h3.ts @@ -1,9 +1,9 @@ import type { H3EventContext } from "./context.ts"; import type { HTTPHandler, EventHandler, Middleware } from "./handler.ts"; import type { HTTPError } from "../error.ts"; -import type { MaybePromise } from "./_utils.ts"; +import type { MaybePromise, RouteParams } from "./_utils.ts"; import type { FetchHandler, ServerRequest } from "srvx"; -import type { MatchedRoute, RouterContext } from "rou3"; +import type { MatchedRoute, InferRouteParams, RouterContext } from "rou3"; import type { H3Event } from "../event.ts"; // --- Misc --- @@ -144,6 +144,14 @@ export declare class H3 extends H3Core { /** * Register a route handler for the specified HTTP method and route. */ + on( + method: HTTPMethod | Lowercase | "", + route: Route, + handler: EventHandler<{ + routerParams: RouteParams>; + }>, + opts?: RouteOptions, + ): this; on( method: HTTPMethod | Lowercase | "", route: string, @@ -170,13 +178,84 @@ export declare class H3 extends H3Core { */ all(route: string, handler: HTTPHandler, opts?: RouteOptions): this; + get( + route: Route, + handler: EventHandler<{ + routerParams: RouteParams>; + }>, + opts?: RouteOptions, + ): this; get(route: string, handler: HTTPHandler, opts?: RouteOptions): this; + + post( + route: Route, + handler: EventHandler<{ + routerParams: RouteParams>; + }>, + opts?: RouteOptions, + ): this; post(route: string, handler: HTTPHandler, opts?: RouteOptions): this; + + put( + route: Route, + handler: EventHandler<{ + routerParams: RouteParams>; + }>, + opts?: RouteOptions, + ): this; put(route: string, handler: HTTPHandler, opts?: RouteOptions): this; + + delete( + route: Route, + handler: EventHandler<{ + routerParams: RouteParams>; + }>, + opts?: RouteOptions, + ): this; delete(route: string, handler: HTTPHandler, opts?: RouteOptions): this; + + patch( + route: Route, + handler: EventHandler<{ + routerParams: RouteParams>; + }>, + opts?: RouteOptions, + ): this; patch(route: string, handler: HTTPHandler, opts?: RouteOptions): this; + + head( + route: Route, + handler: EventHandler<{ + routerParams: RouteParams>; + }>, + opts?: RouteOptions, + ): this; head(route: string, handler: HTTPHandler, opts?: RouteOptions): this; + + options( + route: Route, + handler: EventHandler<{ + routerParams: RouteParams>; + }>, + opts?: RouteOptions, + ): this; options(route: string, handler: HTTPHandler, opts?: RouteOptions): this; + + connect( + route: Route, + handler: EventHandler<{ + routerParams: RouteParams>; + }>, + opts?: RouteOptions, + ): this; connect(route: string, handler: HTTPHandler, opts?: RouteOptions): this; + + trace( + route: Route, + handler: EventHandler<{ + routerParams: RouteParams>; + }>, + opts?: RouteOptions, + ): this; trace(route: string, handler: HTTPHandler, opts?: RouteOptions): this; } From 4b459bcee7ae3d145dd9798ff8a493f086990913 Mon Sep 17 00:00:00 2001 From: Lucas Date: Sun, 12 Oct 2025 06:11:09 +0200 Subject: [PATCH 12/16] test: add tests for route inference --- test/unit/types.test-d.ts | 115 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) diff --git a/test/unit/types.test-d.ts b/test/unit/types.test-d.ts index 452db5abb..b46422b40 100644 --- a/test/unit/types.test-d.ts +++ b/test/unit/types.test-d.ts @@ -1,5 +1,6 @@ /* eslint-disable @typescript-eslint/no-unused-expressions */ import type { H3Event } from "../../src/index.ts"; +import { H3 } from "../../src/index.ts"; import { describe, it, expectTypeOf } from "vitest"; import { defineHandler, @@ -192,4 +193,118 @@ describe("types", () => { }); }); }); + + describe("app route inference", () => { + describe("simple dynamic routes", () => { + it("should infer params from app.get()", () => { + const app = new H3(); + + app.get("/users/:id", (event) => { + expectTypeOf(event.context.params).toEqualTypeOf<{ id: string }>(); + expectTypeOf(event.context.params.id).toEqualTypeOf(); + }); + }); + + it("should infer params from app.post()", () => { + const app = new H3(); + + app.post("/users/:id", (event) => { + expectTypeOf(event.context.params).toEqualTypeOf<{ id: string }>(); + expectTypeOf(event.context.params.id).toEqualTypeOf(); + }); + }); + + it("should not infer params from static route", () => { + const app = new H3(); + + app.get("/about", (event) => { + expectTypeOf(event.context.params).toEqualTypeOf(); + }); + }); + }); + + describe("multiple dynamic segments", () => { + it("should infer params from app.get()", () => { + const app = new H3(); + + app.get("/users/:userId/posts/:postId", (event) => { + expectTypeOf(event.context.params).toEqualTypeOf<{ + userId: string; + postId: string; + }>(); + expectTypeOf(event.context.params.userId).toEqualTypeOf(); + expectTypeOf(event.context.params.postId).toEqualTypeOf(); + }); + }); + + it("should infer params from app.post()", () => { + const app = new H3(); + + app.post("/users/:userId/posts/:postId", (event) => { + expectTypeOf(event.context.params).toEqualTypeOf<{ + userId: string; + postId: string; + }>(); + expectTypeOf(event.context.params.userId).toEqualTypeOf(); + expectTypeOf(event.context.params.postId).toEqualTypeOf(); + }); + }); + }); + + it("should infer params from app.on()", () => { + const app = new H3(); + + app.on("GET", "/products/:productId", (event) => { + expectTypeOf(event.context.params).toEqualTypeOf<{ + productId: string; + }>(); + expectTypeOf(event.context.params.productId).toEqualTypeOf(); + }); + }); + + it("should infer static routes as undefined params", () => { + const app = new H3(); + + app.get("/users", (event) => { + expectTypeOf(event.context.params).toEqualTypeOf(); + }); + + app.post("/users/list", (event) => { + const params = getRouterParams(event); + expectTypeOf(params).toEqualTypeOf(); + }); + }); + + it("should use generic types for reusable handlers", () => { + const app = new H3(); + + const handler = defineHandler((event) => { + expectTypeOf(event.context.params).toEqualTypeOf< + Record | undefined + >(); + + const params = getRouterParams(event); + expectTypeOf(params).toEqualTypeOf< + Record | undefined + >(); + + const id = getRouterParam(event, "id"); + expectTypeOf(id).toEqualTypeOf(); + }); + + app.get("/users/:id", handler); + app.get( + "/posts/:id", + defineHandler((event) => { + expectTypeOf(event.context.params).toEqualTypeOf<{ id: string }>(); + + const params = getRouterParams(event); + expectTypeOf(params).toEqualTypeOf<{ id: string }>(); + + const id = getRouterParam(event, "id"); + expectTypeOf(id).toEqualTypeOf(); + }), + ); + }); + }); }); From 5dc2b5a304156213c5dda43ccabaf4ee7eb700b0 Mon Sep 17 00:00:00 2001 From: Lucas Date: Sun, 12 Oct 2025 06:32:43 +0200 Subject: [PATCH 13/16] refactor(types): simplify route parameter handling This will hopefully clean the types up a bit. --- src/h3.ts | 6 +++--- src/types/_utils.ts | 6 ++++-- src/types/h3.ts | 22 +++++++++++----------- 3 files changed, 18 insertions(+), 16 deletions(-) diff --git a/src/h3.ts b/src/h3.ts index 6b8b4ad2f..8b5712fb7 100644 --- a/src/h3.ts +++ b/src/h3.ts @@ -4,7 +4,7 @@ import { toResponse, kNotFound } from "./response.ts"; import { callMiddleware, normalizeMiddleware } from "./middleware.ts"; import type { ServerRequest } from "srvx"; -import type { RouterContext, MatchedRoute, InferRouteParams } from "rou3"; +import type { RouterContext, MatchedRoute } from "rou3"; import type { H3Config, H3CoreConfig, H3Plugin } from "./types/h3.ts"; import type { H3EventContext } from "./types/context.ts"; import type { @@ -163,7 +163,7 @@ export const H3 = /* @__PURE__ */ (() => { method: HTTPMethod | Lowercase | "", route: Route, handler: EventHandler<{ - routerParams: RouteParams>; + routerParams: RouteParams; }>, opts?: RouteOptions, ): this; @@ -178,7 +178,7 @@ export const H3 = /* @__PURE__ */ (() => { route: Route | string, handler: | EventHandler<{ - routerParams: RouteParams>; + routerParams: RouteParams; }> | HTTPHandler, opts?: RouteOptions, diff --git a/src/types/_utils.ts b/src/types/_utils.ts index 84c64cab0..b980111f0 100644 --- a/src/types/_utils.ts +++ b/src/types/_utils.ts @@ -1,8 +1,10 @@ +import type { InferRouteParams } from "rou3"; + export type MaybePromise = T | Promise; export type Simplify = { [K in keyof T]: T[K] } & {}; -export type RouteParams = - Simplify extends infer _Simplified +export type RouteParams = + Simplify> extends infer _Simplified ? keyof _Simplified extends never ? undefined : _Simplified diff --git a/src/types/h3.ts b/src/types/h3.ts index 94cbeb58b..0dbd07fd4 100644 --- a/src/types/h3.ts +++ b/src/types/h3.ts @@ -3,7 +3,7 @@ import type { HTTPHandler, EventHandler, Middleware } from "./handler.ts"; import type { HTTPError } from "../error.ts"; import type { MaybePromise, RouteParams } from "./_utils.ts"; import type { FetchHandler, ServerRequest } from "srvx"; -import type { MatchedRoute, InferRouteParams, RouterContext } from "rou3"; +import type { MatchedRoute, RouterContext } from "rou3"; import type { H3Event } from "../event.ts"; // --- Misc --- @@ -148,7 +148,7 @@ export declare class H3 extends H3Core { method: HTTPMethod | Lowercase | "", route: Route, handler: EventHandler<{ - routerParams: RouteParams>; + routerParams: RouteParams; }>, opts?: RouteOptions, ): this; @@ -181,7 +181,7 @@ export declare class H3 extends H3Core { get( route: Route, handler: EventHandler<{ - routerParams: RouteParams>; + routerParams: RouteParams; }>, opts?: RouteOptions, ): this; @@ -190,7 +190,7 @@ export declare class H3 extends H3Core { post( route: Route, handler: EventHandler<{ - routerParams: RouteParams>; + routerParams: RouteParams; }>, opts?: RouteOptions, ): this; @@ -199,7 +199,7 @@ export declare class H3 extends H3Core { put( route: Route, handler: EventHandler<{ - routerParams: RouteParams>; + routerParams: RouteParams; }>, opts?: RouteOptions, ): this; @@ -208,7 +208,7 @@ export declare class H3 extends H3Core { delete( route: Route, handler: EventHandler<{ - routerParams: RouteParams>; + routerParams: RouteParams; }>, opts?: RouteOptions, ): this; @@ -217,7 +217,7 @@ export declare class H3 extends H3Core { patch( route: Route, handler: EventHandler<{ - routerParams: RouteParams>; + routerParams: RouteParams; }>, opts?: RouteOptions, ): this; @@ -226,7 +226,7 @@ export declare class H3 extends H3Core { head( route: Route, handler: EventHandler<{ - routerParams: RouteParams>; + routerParams: RouteParams; }>, opts?: RouteOptions, ): this; @@ -235,7 +235,7 @@ export declare class H3 extends H3Core { options( route: Route, handler: EventHandler<{ - routerParams: RouteParams>; + routerParams: RouteParams; }>, opts?: RouteOptions, ): this; @@ -244,7 +244,7 @@ export declare class H3 extends H3Core { connect( route: Route, handler: EventHandler<{ - routerParams: RouteParams>; + routerParams: RouteParams; }>, opts?: RouteOptions, ): this; @@ -253,7 +253,7 @@ export declare class H3 extends H3Core { trace( route: Route, handler: EventHandler<{ - routerParams: RouteParams>; + routerParams: RouteParams; }>, opts?: RouteOptions, ): this; From 0846190bd46f2f8f31ad6bb71303241209a8991f Mon Sep 17 00:00:00 2001 From: Lucas Date: Sun, 12 Oct 2025 06:39:26 +0200 Subject: [PATCH 14/16] refactor(types): simplify route type definitions * Removed redundant `const` keyword from route type parameters in `on`, `get`, `post`, `put`, `delete`, `patch`, `head`, `options`, `connect`, and `trace` methods. * Improved type inference for route parameters. --- src/h3.ts | 4 ++-- src/types/h3.ts | 27 +++++++++++++++++---------- 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/src/h3.ts b/src/h3.ts index 8b5712fb7..ece326a53 100644 --- a/src/h3.ts +++ b/src/h3.ts @@ -159,7 +159,7 @@ export const H3 = /* @__PURE__ */ (() => { return this; } - on( + on( method: HTTPMethod | Lowercase | "", route: Route, handler: EventHandler<{ @@ -173,7 +173,7 @@ export const H3 = /* @__PURE__ */ (() => { handler: HTTPHandler, opts?: RouteOptions, ): this; - on( + on( method: HTTPMethod | Lowercase | "", route: Route | string, handler: diff --git a/src/types/h3.ts b/src/types/h3.ts index 0dbd07fd4..a7f7d7091 100644 --- a/src/types/h3.ts +++ b/src/types/h3.ts @@ -144,7 +144,7 @@ export declare class H3 extends H3Core { /** * Register a route handler for the specified HTTP method and route. */ - on( + on( method: HTTPMethod | Lowercase | "", route: Route, handler: EventHandler<{ @@ -176,9 +176,16 @@ export declare class H3 extends H3Core { /** * Register a route handler for all HTTP methods. */ + all( + route: Route, + handler: EventHandler<{ + routerParams: RouteParams; + }>, + opts?: RouteOptions, + ): this; all(route: string, handler: HTTPHandler, opts?: RouteOptions): this; - get( + get( route: Route, handler: EventHandler<{ routerParams: RouteParams; @@ -187,7 +194,7 @@ export declare class H3 extends H3Core { ): this; get(route: string, handler: HTTPHandler, opts?: RouteOptions): this; - post( + post( route: Route, handler: EventHandler<{ routerParams: RouteParams; @@ -196,7 +203,7 @@ export declare class H3 extends H3Core { ): this; post(route: string, handler: HTTPHandler, opts?: RouteOptions): this; - put( + put( route: Route, handler: EventHandler<{ routerParams: RouteParams; @@ -205,7 +212,7 @@ export declare class H3 extends H3Core { ): this; put(route: string, handler: HTTPHandler, opts?: RouteOptions): this; - delete( + delete( route: Route, handler: EventHandler<{ routerParams: RouteParams; @@ -214,7 +221,7 @@ export declare class H3 extends H3Core { ): this; delete(route: string, handler: HTTPHandler, opts?: RouteOptions): this; - patch( + patch( route: Route, handler: EventHandler<{ routerParams: RouteParams; @@ -223,7 +230,7 @@ export declare class H3 extends H3Core { ): this; patch(route: string, handler: HTTPHandler, opts?: RouteOptions): this; - head( + head( route: Route, handler: EventHandler<{ routerParams: RouteParams; @@ -232,7 +239,7 @@ export declare class H3 extends H3Core { ): this; head(route: string, handler: HTTPHandler, opts?: RouteOptions): this; - options( + options( route: Route, handler: EventHandler<{ routerParams: RouteParams; @@ -241,7 +248,7 @@ export declare class H3 extends H3Core { ): this; options(route: string, handler: HTTPHandler, opts?: RouteOptions): this; - connect( + connect( route: Route, handler: EventHandler<{ routerParams: RouteParams; @@ -250,7 +257,7 @@ export declare class H3 extends H3Core { ): this; connect(route: string, handler: HTTPHandler, opts?: RouteOptions): this; - trace( + trace( route: Route, handler: EventHandler<{ routerParams: RouteParams; From 42a9512d42e61499d96ac0dcc40a9da56220cb99 Mon Sep 17 00:00:00 2001 From: Lucas Date: Sun, 12 Oct 2025 07:30:40 +0200 Subject: [PATCH 15/16] refactor(types): cleanup handler types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This should make the h3 declared class not look too complex with the overloads. I couldn't get the `all` to use the H3HandlerInterface, some type errors in H3Core appeared 😅 --- src/types/h3.ts | 117 ++++++++++++-------------------------- test/unit/types.test-d.ts | 7 +++ 2 files changed, 42 insertions(+), 82 deletions(-) diff --git a/src/types/h3.ts b/src/types/h3.ts index a7f7d7091..147bfc50f 100644 --- a/src/types/h3.ts +++ b/src/types/h3.ts @@ -10,8 +10,32 @@ import type { H3Event } from "../event.ts"; // https://www.rfc-editor.org/rfc/rfc7231#section-4.1 // prettier-ignore -export type HTTPMethod = "GET" | "HEAD" | "PATCH" | "POST" | "PUT" | "DELETE" | "CONNECT" | "OPTIONS" | "TRACE"; - +export type HTTPMethod = "GET" | "HEAD" | "PATCH" | "POST" | "PUT" | "DELETE" | "CONNECT" | "OPTIONS" | "TRACE"; + +/** + * Interface for HTTP method handlers (GET, POST, PUT, DELETE, etc.). + * + * Automatically infers route parameters from the route pattern and makes them + * available in the event handler context. + * + * @template {H3} This - Passed as `this` to resolve to the declared H3 class type + * rather than the runtime H3 class implementation. This ensures TypeScript uses + * the correct type signature from this declaration file. + * + * NOTE: + * If we used H3 directly in the return type, the bench implementation would + * fail, due to `app._rou3` not being defined on H3. + */ +interface H3HandlerInterface { + ( + route: Route, + handler: EventHandler<{ + routerParams: RouteParams; + }>, + opts?: RouteOptions, + ): This; + (route: string, handler: HTTPHandler, opts?: RouteOptions): This; +} export interface H3Config { /** * When enabled, H3 displays debugging stack traces in HTTP responses (potentially dangerous for production!). @@ -185,84 +209,13 @@ export declare class H3 extends H3Core { ): this; all(route: string, handler: HTTPHandler, opts?: RouteOptions): this; - get( - route: Route, - handler: EventHandler<{ - routerParams: RouteParams; - }>, - opts?: RouteOptions, - ): this; - get(route: string, handler: HTTPHandler, opts?: RouteOptions): this; - - post( - route: Route, - handler: EventHandler<{ - routerParams: RouteParams; - }>, - opts?: RouteOptions, - ): this; - post(route: string, handler: HTTPHandler, opts?: RouteOptions): this; - - put( - route: Route, - handler: EventHandler<{ - routerParams: RouteParams; - }>, - opts?: RouteOptions, - ): this; - put(route: string, handler: HTTPHandler, opts?: RouteOptions): this; - - delete( - route: Route, - handler: EventHandler<{ - routerParams: RouteParams; - }>, - opts?: RouteOptions, - ): this; - delete(route: string, handler: HTTPHandler, opts?: RouteOptions): this; - - patch( - route: Route, - handler: EventHandler<{ - routerParams: RouteParams; - }>, - opts?: RouteOptions, - ): this; - patch(route: string, handler: HTTPHandler, opts?: RouteOptions): this; - - head( - route: Route, - handler: EventHandler<{ - routerParams: RouteParams; - }>, - opts?: RouteOptions, - ): this; - head(route: string, handler: HTTPHandler, opts?: RouteOptions): this; - - options( - route: Route, - handler: EventHandler<{ - routerParams: RouteParams; - }>, - opts?: RouteOptions, - ): this; - options(route: string, handler: HTTPHandler, opts?: RouteOptions): this; - - connect( - route: Route, - handler: EventHandler<{ - routerParams: RouteParams; - }>, - opts?: RouteOptions, - ): this; - connect(route: string, handler: HTTPHandler, opts?: RouteOptions): this; - - trace( - route: Route, - handler: EventHandler<{ - routerParams: RouteParams; - }>, - opts?: RouteOptions, - ): this; - trace(route: string, handler: HTTPHandler, opts?: RouteOptions): this; + get: H3HandlerInterface; + post: H3HandlerInterface; + put: H3HandlerInterface; + delete: H3HandlerInterface; + patch: H3HandlerInterface; + head: H3HandlerInterface; + options: H3HandlerInterface; + connect: H3HandlerInterface; + trace: H3HandlerInterface; } diff --git a/test/unit/types.test-d.ts b/test/unit/types.test-d.ts index b46422b40..f8b817485 100644 --- a/test/unit/types.test-d.ts +++ b/test/unit/types.test-d.ts @@ -305,6 +305,13 @@ describe("types", () => { expectTypeOf(id).toEqualTypeOf(); }), ); + + app.all("/:userid/:postId", (event) => { + expectTypeOf(event.context.params).toEqualTypeOf<{ + userid: string; + postId: string; + }>(); + }); }); }); }); From 931281afe04e508a2c68f3e2e02756951f4525cb Mon Sep 17 00:00:00 2001 From: Lucas Date: Sun, 12 Oct 2025 07:31:39 +0200 Subject: [PATCH 16/16] chore: remove misplaced test --- test/unit/types.test-d.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/test/unit/types.test-d.ts b/test/unit/types.test-d.ts index f8b817485..b46422b40 100644 --- a/test/unit/types.test-d.ts +++ b/test/unit/types.test-d.ts @@ -305,13 +305,6 @@ describe("types", () => { expectTypeOf(id).toEqualTypeOf(); }), ); - - app.all("/:userid/:postId", (event) => { - expectTypeOf(event.context.params).toEqualTypeOf<{ - userid: string; - postId: string; - }>(); - }); }); }); });