Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
21 changes: 21 additions & 0 deletions src/types/_utils.ts
Original file line number Diff line number Diff line change
@@ -1 +1,22 @@
export type MaybePromise<T = unknown> = T | Promise<T>;

/**
* 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<T extends string> = _Prettify<_ExtractParams<T>>;

type _ExtractParams<T extends string> = string extends T
? Record<string, string>
: T extends `${string}:${infer Param}/${infer Rest}`
? { [K in Param]: string } & _ExtractParams<Rest>
: T extends `${string}:${infer Param}`
? { [K in Param]: string }
: Record<string, string>;

type _Prettify<T> = { [K in keyof T]: T[K] } & {};
54 changes: 52 additions & 2 deletions src/types/h3.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -173,15 +173,65 @@ export declare class H3 extends H3Core {
/**
* Register a route handler for all HTTP methods.
*/
all<P extends string>(
route: P,
handler: (event: H3Event<{ routerParams: ExtractRouteParams<P> }>) => unknown,
opts?: RouteOptions,
): this;
all(route: string, handler: HTTPHandler, opts?: RouteOptions): this;

get<P extends string>(
route: P,
handler: (event: H3Event<{ routerParams: ExtractRouteParams<P> }>) => unknown,
opts?: RouteOptions,
): this;
get(route: string, handler: HTTPHandler, opts?: RouteOptions): this;
post<P extends string>(
route: P,
handler: (event: H3Event<{ routerParams: ExtractRouteParams<P> }>) => unknown,
opts?: RouteOptions,
): this;
post(route: string, handler: HTTPHandler, opts?: RouteOptions): this;
put<P extends string>(
route: P,
handler: (event: H3Event<{ routerParams: ExtractRouteParams<P> }>) => unknown,
opts?: RouteOptions,
): this;
put(route: string, handler: HTTPHandler, opts?: RouteOptions): this;
delete<P extends string>(
route: P,
handler: (event: H3Event<{ routerParams: ExtractRouteParams<P> }>) => unknown,
opts?: RouteOptions,
): this;
delete(route: string, handler: HTTPHandler, opts?: RouteOptions): this;
patch<P extends string>(
route: P,
handler: (event: H3Event<{ routerParams: ExtractRouteParams<P> }>) => unknown,
opts?: RouteOptions,
): this;
patch(route: string, handler: HTTPHandler, opts?: RouteOptions): this;
head<P extends string>(
route: P,
handler: (event: H3Event<{ routerParams: ExtractRouteParams<P> }>) => unknown,
opts?: RouteOptions,
): this;
head(route: string, handler: HTTPHandler, opts?: RouteOptions): this;
options<P extends string>(
route: P,
handler: (event: H3Event<{ routerParams: ExtractRouteParams<P> }>) => unknown,
opts?: RouteOptions,
): this;
options(route: string, handler: HTTPHandler, opts?: RouteOptions): this;
connect<P extends string>(
route: P,
handler: (event: H3Event<{ routerParams: ExtractRouteParams<P> }>) => unknown,
opts?: RouteOptions,
): this;
connect(route: string, handler: HTTPHandler, opts?: RouteOptions): this;
trace<P extends string>(
route: P,
handler: (event: H3Event<{ routerParams: ExtractRouteParams<P> }>) => unknown,
opts?: RouteOptions,
): this;
trace(route: string, handler: HTTPHandler, opts?: RouteOptions): this;
}
13 changes: 7 additions & 6 deletions src/utils/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,10 +138,11 @@ export function getValidatedQuery(
* const params = getRouterParams(event); // { key: "value" }
* });
*/
export function getRouterParams(
event: HTTPEvent,
opts: { decode?: boolean } = {},
): NonNullable<H3Event["context"]["params"]> {
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<H3EventContext>(event);
let params = (context.params || {}) as NonNullable<H3Event["context"]["params"]>;
Expand All @@ -151,7 +152,7 @@ export function getRouterParams(
params[key] = decodeURIComponent(params[key]);
}
}
return params;
return params as _T;
}

export function getValidatedRouterParams<Event extends HTTPEvent, S extends StandardSchemaV1>(
Expand Down Expand Up @@ -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<string, string>;
return params[name];
}

Expand Down
52 changes: 51 additions & 1 deletion test/unit/types.test-d.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -123,4 +125,52 @@ describe("types", () => {
});
});
});

describe("ExtractRouteParams", () => {
it("extracts single param", () => {
expectTypeOf<ExtractRouteParams<"/users/:id">>().toEqualTypeOf<{
id: string;
}>();
});

it("extracts multiple params", () => {
expectTypeOf<ExtractRouteParams<"/users/:id/posts/:slug">>().toEqualTypeOf<{
id: string;
slug: string;
}>();
});

it("returns Record<string, string> for no params", () => {
expectTypeOf<ExtractRouteParams<"/users">>().toEqualTypeOf<Record<string, string>>();
});
});

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<string, string> for static route", () => {
const app = new H3();
app.get("/users", (event) => {
const params = getRouterParams(event);
expectTypeOf(params).toEqualTypeOf<Record<string, string>>();
});
});
});
});
Loading