Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,6 @@ jobs:
- run: pnpm install
- run: pnpm lint
- run: pnpm build
- run: pnpm test:types
- run: pnpm vitest --coverage && rm -rf coverage/tmp
- uses: codecov/codecov-action@v3
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@
"play": "jiti ./playground/index.ts",
"profile": "0x -o -D .profile -P 'autocannon -c 100 -p 10 -d 40 http://localhost:$PORT' ./playground/server.cjs",
"release": "pnpm test && pnpm build && changelogen --release && pnpm publish && git push --follow-tags",
"test": "pnpm lint && vitest run --coverage"
"test": "pnpm lint && vitest run --coverage",
"test:types": "vitest typecheck"
},
"dependencies": {
"cookie-es": "^1.0.0",
Expand Down Expand Up @@ -61,4 +62,4 @@
"vitest": "^0.32.2"
},
"packageManager": "pnpm@8.6.3"
}
}
6 changes: 4 additions & 2 deletions src/event/event.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { H3EventContext } from "../types";
import type { H3EventContext, TypedEventInputSignature } from "../types";
import type { NodeIncomingMessage, NodeServerResponse } from "../node";
import {
MIMES,
Expand All @@ -13,7 +13,9 @@ export interface NodeEventContext {
res: NodeServerResponse;
}

export class H3Event implements Pick<FetchEvent, "respondWith"> {
export class H3Event<_Input extends TypedEventInputSignature = any>
implements Pick<FetchEvent, "respondWith">
{
"__is_event__" = true;
_handled = false;

Expand Down
28 changes: 24 additions & 4 deletions src/event/utils.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,28 @@
import type { EventHandler, LazyEventHandler } from "../types";
import type {
EventHandler,
LazyEventHandler,
TypedEventInputSignature,
} from "../types";

export function defineEventHandler<T = any>(
handler: EventHandler<T>
): EventHandler<T> {
export function defineEventHandler<
Input extends TypedEventInputSignature = any,
Return = any
>(handler: EventHandler<Input, Return>): EventHandler<Input, Return>;
// TODO: remove when appropriate
// This signature provides backwards compatibility with previous signature where first generic was return type
export function defineEventHandler<Input = any, Return = any>(
handler: EventHandler<
Input extends TypedEventInputSignature ? Input : any,
Input extends TypedEventInputSignature ? Return : Input
>
): EventHandler<
Input extends TypedEventInputSignature ? Input : any,
Input extends TypedEventInputSignature ? Return : Input
>;
export function defineEventHandler<
Input extends TypedEventInputSignature = any,
Return = any
>(handler: EventHandler<Input, Return>): EventHandler<Input, Return> {
handler.__is_handler__ = true;
return handler;
}
Expand Down
12 changes: 10 additions & 2 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,17 @@ export interface H3EventContext extends Record<string, any> {

export type EventHandlerResponse<T = any> = T | Promise<T>;

export interface EventHandler<T = any> {
export interface TypedEventInputSignature {
body?: any;
query?: any;
}

export interface EventHandler<
Input extends TypedEventInputSignature = any,
Return = any
> {
__is_handler__?: true;
(event: H3Event): EventHandlerResponse<T>;
(event: H3Event<Input>): EventHandlerResponse<Return>;
}

export type LazyEventHandler = () => EventHandler | Promise<EventHandler>;
Expand Down
22 changes: 19 additions & 3 deletions src/utils/body.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,15 @@ export function readRawBody<E extends Encoding = "utf8">(
* const body = await readBody(req)
* ```
*/
export async function readBody<T = any>(event: H3Event): Promise<T> {
export async function readBody<T = unknown, E extends H3Event = H3Event>(
event: E
): Promise<
unknown extends T
? E extends H3Event<infer Input>
? Input["body"]
: never
: T
> {
if (ParsedBodySymbol in event.node.req) {
return (event.node.req as any)[ParsedBodySymbol];
}
Expand All @@ -106,12 +114,20 @@ export async function readBody<T = any>(event: H3Event): Promise<T> {
parsedForm[key] = value;
}
}
return parsedForm as unknown as T;
return parsedForm as unknown as unknown extends T
Comment thread
pi0 marked this conversation as resolved.
Outdated
? E extends H3Event<infer Input>
? Input["body"]
: never
: T;
}

const json = destr(body) as T;
(event.node.req as any)[ParsedBodySymbol] = json;
return json;
return json as unknown extends T
? E extends H3Event<infer Input>
? Input["body"]
: never
: T;
}

export async function readMultipartFormData(event: H3Event) {
Expand Down
6 changes: 4 additions & 2 deletions src/utils/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ import { createError } from "../error";
import type { HTTPMethod, RequestHeaders } from "../types";
import type { H3Event } from "../event";

export function getQuery(event: H3Event) {
return _getQuery(event.node.req.url || "");
export function getQuery<E extends H3Event = H3Event>(event: E) {
return _getQuery(event.node.req.url || "") as E extends H3Event<infer Input>
? Input["query"]
: any;
}

export function getRouterParams(
Expand Down
53 changes: 53 additions & 0 deletions test/types.test-d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { describe, it, expectTypeOf } from "vitest";
import { eventHandler, H3Event, readBody, getQuery } from "../src";

describe("types for event handlers", () => {
it("return type test", () => {
const handler = eventHandler(() => {
return {
foo: "bar",
};
});

expectTypeOf(handler({} as H3Event)).toEqualTypeOf<
{ foo: string } | Promise<{ foo: string }>
>();
});

it("input type test", () => {
eventHandler<{ body: { id: string } }>(async (event) => {
const body = await readBody(event);
expectTypeOf(body).toEqualTypeOf<{ id: string }>();
expectTypeOf(getQuery(event)).toBeUnknown();

return null;
});

eventHandler<{ query: { id: string } }>((event) => {
const query = getQuery(event);
expectTypeOf(query).toEqualTypeOf<{ id: string }>();

return null;
});
});

it("allows backwards compatible generic for eventHandler definition", () => {
const handler = eventHandler<string>(() => {
return "";
});
expectTypeOf(handler({} as H3Event)).toEqualTypeOf<
string | Promise<string>
>();
});

// For backwards compatibility - this should likely become `unknown` in future
it("input types aren't applied when omitted", () => {
eventHandler(async (event) => {
const body = await readBody(event);
expectTypeOf(body).toBeAny();
expectTypeOf(getQuery(event)).toBeAny();

return null;
});
});
});
5 changes: 4 additions & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
"baseUrl": ".",
"target": "ESNext",
"module": "ESNext",
"skipLibCheck": true,
"allowSyntheticDefaultImports": true,
"moduleResolution": "Node",
"lib": [
"WebWorker",
Expand All @@ -16,6 +18,7 @@
]
},
"include": [
"src"
"src",
"test/types.test-d.ts"
]
}