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..dd67538ff 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,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: 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..a78b73677 100644 --- a/src/utils/request.ts +++ b/src/utils/request.ts @@ -138,10 +138,11 @@ export function getValidatedQuery( * const params = getRouterParams(event); // { key: "value" } * }); */ -export function getRouterParams( - event: HTTPEvent, - opts: { decode?: boolean } = {}, -): NonNullable { +export function getRouterParams< + T, + _Event extends HTTPEvent = HTTPEvent, + _T = InferEventInput<"routerParams", _Event, 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; @@ -151,7 +152,7 @@ export function getRouterParams( params[key] = decodeURIComponent(params[key]); } } - return params; + return params as _T; } export function getValidatedRouterParams( @@ -252,7 +253,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..3c01ad7a5 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,52 @@ describe("types", () => { }); }); }); + + describe("ExtractRouteParams", () => { + it("extracts single param", () => { + expectTypeOf>().toEqualTypeOf<{ + id: string; + }>(); + }); + + it("extracts multiple params", () => { + expectTypeOf>().toEqualTypeOf<{ + id: string; + slug: string; + }>(); + }); + + it("returns Record for no params", () => { + expectTypeOf>().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>(); + }); + }); + }); });