From 91371811f6cced65945e6c7f69bbab6c57b236d3 Mon Sep 17 00:00:00 2001 From: productdevbook Date: Sun, 15 Mar 2026 00:06:06 +0300 Subject: [PATCH 1/2] feat(types): add ExtractRouteParams and route param inference MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add ExtractRouteParams utility type that extracts parameter names from route path strings at compile time: type P = ExtractRouteParams<"/users/:id/posts/:slug"> // { id: string; slug: string } Route methods (get, post, put, etc.) now infer route params from the path string literal and pass them to the handler's event type. Combined with the updated getRouterParams (now uses InferEventInput), this gives automatic type inference: app.get("/users/:id", (event) => { const params = getRouterParams(event); // params is { id: string } — inferred from path }); Zero runtime changes — purely type-level improvement. Closes #1053 Co-Authored-By: Claude Opus 4.6 (1M context) --- src/index.ts | 2 ++ src/types/_utils.ts | 21 ++++++++++++++++ src/types/h3.ts | 14 +++++++++-- src/utils/request.ts | 14 +++++++---- test/unit/types.test-d.ts | 53 ++++++++++++++++++++++++++++++++++++++- 5 files changed, 96 insertions(+), 8 deletions(-) diff --git a/src/index.ts b/src/index.ts index 827dedddd..27b155503 100644 --- a/src/index.ts +++ b/src/index.ts @@ -26,6 +26,8 @@ export { isEvent, isHTTPEvent, mockEvent, getEventContext } from "./utils/event. // Handler and Middleware +export type { ExtractRouteParams } from "./types/_utils.ts"; + export type { EventHandler, DynamicEventHandler, diff --git a/src/types/_utils.ts b/src/types/_utils.ts index 4773ec36c..ea75d88b5 100644 --- a/src/types/_utils.ts +++ b/src/types/_utils.ts @@ -1 +1,22 @@ export type MaybePromise = T | Promise; + +/** + * Extract route parameter names from a route path string. + * + * @example + * ```ts + * type Params = ExtractRouteParams<"/users/:id/posts/:slug">; + * // { id: string; slug: string } + * ``` + */ +export type ExtractRouteParams = _Prettify<_ExtractParams>; + +type _ExtractParams = string extends T + ? Record + : T extends `${string}:${infer Param}/${infer Rest}` + ? { [K in Param]: string } & _ExtractParams + : T extends `${string}:${infer Param}` + ? { [K in Param]: string } + : Record; + +type _Prettify = { [K in keyof T]: T[K] } & {}; diff --git a/src/types/h3.ts b/src/types/h3.ts index fae2f8c19..c259c3c8b 100644 --- a/src/types/h3.ts +++ b/src/types/h3.ts @@ -1,7 +1,7 @@ import type { H3EventContext } from "./context.ts"; -import type { HTTPHandler, EventHandler, Middleware } from "./handler.ts"; +import type { HTTPHandler, EventHandler, Middleware, EventHandlerResponse } from "./handler.ts"; import type { HTTPError } from "../error.ts"; -import type { MaybePromise } from "./_utils.ts"; +import type { MaybePromise, ExtractRouteParams } from "./_utils.ts"; import type { FetchHandler, ServerRequest } from "srvx"; // import type { MatchedRoute, RouterContext } from "rou3"; import type { H3Event } from "../event.ts"; @@ -173,15 +173,25 @@ export declare class H3 extends H3Core { /** * Register a route handler for all HTTP methods. */ + all

(route: P, handler: (event: H3Event<{ routerParams: ExtractRouteParams

}>) => unknown, opts?: RouteOptions): this; all(route: string, handler: HTTPHandler, opts?: RouteOptions): this; + get

(route: P, handler: (event: H3Event<{ routerParams: ExtractRouteParams

}>) => unknown, opts?: RouteOptions): this; get(route: string, handler: HTTPHandler, opts?: RouteOptions): this; + post

(route: P, handler: (event: H3Event<{ routerParams: ExtractRouteParams

}>) => unknown, opts?: RouteOptions): this; post(route: string, handler: HTTPHandler, opts?: RouteOptions): this; + put

(route: P, handler: (event: H3Event<{ routerParams: ExtractRouteParams

}>) => unknown, opts?: RouteOptions): this; put(route: string, handler: HTTPHandler, opts?: RouteOptions): this; + delete

(route: P, handler: (event: H3Event<{ routerParams: ExtractRouteParams

}>) => unknown, opts?: RouteOptions): this; delete(route: string, handler: HTTPHandler, opts?: RouteOptions): this; + patch

(route: P, handler: (event: H3Event<{ routerParams: ExtractRouteParams

}>) => unknown, opts?: RouteOptions): this; patch(route: string, handler: HTTPHandler, opts?: RouteOptions): this; + head

(route: P, handler: (event: H3Event<{ routerParams: ExtractRouteParams

}>) => unknown, opts?: RouteOptions): this; head(route: string, handler: HTTPHandler, opts?: RouteOptions): this; + options

(route: P, handler: (event: H3Event<{ routerParams: ExtractRouteParams

}>) => unknown, opts?: RouteOptions): this; options(route: string, handler: HTTPHandler, opts?: RouteOptions): this; + connect

(route: P, handler: (event: H3Event<{ routerParams: ExtractRouteParams

}>) => unknown, opts?: RouteOptions): this; connect(route: string, handler: HTTPHandler, opts?: RouteOptions): this; + trace

(route: P, handler: (event: H3Event<{ routerParams: ExtractRouteParams

}>) => unknown, opts?: RouteOptions): this; trace(route: string, handler: HTTPHandler, opts?: RouteOptions): this; } diff --git a/src/utils/request.ts b/src/utils/request.ts index 311ff0002..23f355e58 100644 --- a/src/utils/request.ts +++ b/src/utils/request.ts @@ -138,10 +138,14 @@ export function getValidatedQuery( * const params = getRouterParams(event); // { key: "value" } * }); */ -export function getRouterParams( - event: HTTPEvent, +export function getRouterParams< + T, + _Event extends HTTPEvent = HTTPEvent, + _T = InferEventInput<"routerParams", _Event, T>, +>( + event: _Event, opts: { decode?: boolean } = {}, -): NonNullable { +): _T { // Fallback object needs to be returned in case router is not used (#149) const context = getEventContext(event); let params = (context.params || {}) as NonNullable; @@ -151,7 +155,7 @@ export function getRouterParams( params[key] = decodeURIComponent(params[key]); } } - return params; + return params as _T; } export function getValidatedRouterParams( @@ -252,7 +256,7 @@ export function getRouterParam( name: string, opts: { decode?: boolean } = {}, ): string | undefined { - const params = getRouterParams(event, opts); + const params = getRouterParams(event, opts) as Record; return params[name]; } diff --git a/test/unit/types.test-d.ts b/test/unit/types.test-d.ts index e34682b33..6bc2504b7 100644 --- a/test/unit/types.test-d.ts +++ b/test/unit/types.test-d.ts @@ -1,8 +1,10 @@ -import type { H3Event } from "../../src/index.ts"; +import type { H3Event, ExtractRouteParams } from "../../src/index.ts"; import { describe, it, expectTypeOf } from "vitest"; import { + H3, defineHandler, getQuery, + getRouterParams, readBody, readValidatedBody, getValidatedQuery, @@ -123,4 +125,53 @@ describe("types", () => { }); }); }); + + describe("ExtractRouteParams", () => { + it("extracts single param", () => { + expectTypeOf>().toEqualTypeOf<{ + id: string; + }>(); + }); + + it("extracts multiple params", () => { + expectTypeOf< + ExtractRouteParams<"/users/:id/posts/:slug"> + >().toEqualTypeOf<{ id: string; slug: string }>(); + }); + + it("returns Record for no params", () => { + expectTypeOf< + ExtractRouteParams<"/users"> + >().toEqualTypeOf>(); + }); + }); + + describe("route param inference", () => { + it("infers params from app.get path", () => { + const app = new H3(); + app.get("/users/:id", (event) => { + const params = getRouterParams(event); + expectTypeOf(params).toEqualTypeOf<{ id: string }>(); + }); + }); + + it("infers multiple params from app.post path", () => { + const app = new H3(); + app.post("/users/:userId/posts/:postId", (event) => { + const params = getRouterParams(event); + expectTypeOf(params).toEqualTypeOf<{ + userId: string; + postId: string; + }>(); + }); + }); + + it("infers Record for static route", () => { + const app = new H3(); + app.get("/users", (event) => { + const params = getRouterParams(event); + expectTypeOf(params).toEqualTypeOf>(); + }); + }); + }); }); From 3a726c6236285b40832c25926f5c0df889db0db8 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sat, 14 Mar 2026 21:06:36 +0000 Subject: [PATCH 2/2] chore: apply automated updates --- src/types/h3.ts | 60 ++++++++++++++++++++++++++++++++------- src/utils/request.ts | 5 +--- test/unit/types.test-d.ts | 11 ++++--- 3 files changed, 56 insertions(+), 20 deletions(-) diff --git a/src/types/h3.ts b/src/types/h3.ts index c259c3c8b..dd67538ff 100644 --- a/src/types/h3.ts +++ b/src/types/h3.ts @@ -173,25 +173,65 @@ export declare class H3 extends H3Core { /** * Register a route handler for all HTTP methods. */ - all

(route: P, handler: (event: H3Event<{ routerParams: ExtractRouteParams

}>) => unknown, opts?: RouteOptions): this; + all

( + route: P, + handler: (event: H3Event<{ routerParams: ExtractRouteParams

}>) => unknown, + opts?: RouteOptions, + ): this; all(route: string, handler: HTTPHandler, opts?: RouteOptions): this; - get

(route: P, handler: (event: H3Event<{ routerParams: ExtractRouteParams

}>) => unknown, opts?: RouteOptions): this; + get

( + route: P, + handler: (event: H3Event<{ routerParams: ExtractRouteParams

}>) => unknown, + opts?: RouteOptions, + ): this; get(route: string, handler: HTTPHandler, opts?: RouteOptions): this; - post

(route: P, handler: (event: H3Event<{ routerParams: ExtractRouteParams

}>) => unknown, opts?: RouteOptions): this; + post

( + route: P, + handler: (event: H3Event<{ routerParams: ExtractRouteParams

}>) => unknown, + opts?: RouteOptions, + ): this; post(route: string, handler: HTTPHandler, opts?: RouteOptions): this; - put

(route: P, handler: (event: H3Event<{ routerParams: ExtractRouteParams

}>) => unknown, opts?: RouteOptions): this; + put

( + route: P, + handler: (event: H3Event<{ routerParams: ExtractRouteParams

}>) => unknown, + opts?: RouteOptions, + ): this; put(route: string, handler: HTTPHandler, opts?: RouteOptions): this; - delete

(route: P, handler: (event: H3Event<{ routerParams: ExtractRouteParams

}>) => unknown, opts?: RouteOptions): this; + delete

( + route: P, + handler: (event: H3Event<{ routerParams: ExtractRouteParams

}>) => unknown, + opts?: RouteOptions, + ): this; delete(route: string, handler: HTTPHandler, opts?: RouteOptions): this; - patch

(route: P, handler: (event: H3Event<{ routerParams: ExtractRouteParams

}>) => unknown, opts?: RouteOptions): this; + patch

( + route: P, + handler: (event: H3Event<{ routerParams: ExtractRouteParams

}>) => unknown, + opts?: RouteOptions, + ): this; patch(route: string, handler: HTTPHandler, opts?: RouteOptions): this; - head

(route: P, handler: (event: H3Event<{ routerParams: ExtractRouteParams

}>) => unknown, opts?: RouteOptions): this; + head

( + route: P, + handler: (event: H3Event<{ routerParams: ExtractRouteParams

}>) => unknown, + opts?: RouteOptions, + ): this; head(route: string, handler: HTTPHandler, opts?: RouteOptions): this; - options

(route: P, handler: (event: H3Event<{ routerParams: ExtractRouteParams

}>) => unknown, opts?: RouteOptions): this; + options

( + route: P, + handler: (event: H3Event<{ routerParams: ExtractRouteParams

}>) => unknown, + opts?: RouteOptions, + ): this; options(route: string, handler: HTTPHandler, opts?: RouteOptions): this; - connect

(route: P, handler: (event: H3Event<{ routerParams: ExtractRouteParams

}>) => unknown, opts?: RouteOptions): this; + connect

( + route: P, + handler: (event: H3Event<{ routerParams: ExtractRouteParams

}>) => unknown, + opts?: RouteOptions, + ): this; connect(route: string, handler: HTTPHandler, opts?: RouteOptions): this; - trace

(route: P, handler: (event: H3Event<{ routerParams: ExtractRouteParams

}>) => unknown, opts?: RouteOptions): this; + trace

( + route: P, + handler: (event: H3Event<{ routerParams: ExtractRouteParams

}>) => unknown, + opts?: RouteOptions, + ): this; trace(route: string, handler: HTTPHandler, opts?: RouteOptions): this; } diff --git a/src/utils/request.ts b/src/utils/request.ts index 23f355e58..a78b73677 100644 --- a/src/utils/request.ts +++ b/src/utils/request.ts @@ -142,10 +142,7 @@ export function getRouterParams< T, _Event extends HTTPEvent = HTTPEvent, _T = InferEventInput<"routerParams", _Event, T>, ->( - event: _Event, - opts: { decode?: boolean } = {}, -): _T { +>(event: _Event, opts: { decode?: boolean } = {}): _T { // Fallback object needs to be returned in case router is not used (#149) const context = getEventContext(event); let params = (context.params || {}) as NonNullable; diff --git a/test/unit/types.test-d.ts b/test/unit/types.test-d.ts index 6bc2504b7..3c01ad7a5 100644 --- a/test/unit/types.test-d.ts +++ b/test/unit/types.test-d.ts @@ -134,15 +134,14 @@ describe("types", () => { }); it("extracts multiple params", () => { - expectTypeOf< - ExtractRouteParams<"/users/:id/posts/:slug"> - >().toEqualTypeOf<{ id: string; slug: string }>(); + expectTypeOf>().toEqualTypeOf<{ + id: string; + slug: string; + }>(); }); it("returns Record for no params", () => { - expectTypeOf< - ExtractRouteParams<"/users"> - >().toEqualTypeOf>(); + expectTypeOf>().toEqualTypeOf>(); }); });