kwhitley / itty-router

A little router.
MIT License
1.7k stars 77 forks source link

Propagate handler argument types down using generics instead of `any` #154

Closed 3x071c closed 1 year ago

3x071c commented 1 year ago

Thanks for the great work here. I'm using your router (version 4/next.46) with Cloudflare Workers and there are some environment variables and a custom context object I'd like to pass into router.handle as the second/third argument (as illustrated in some of your own examples). This works fine from a JavaScript standpoint, but the types are bad. Every RouteHandler function receives all arguments except the first (the properly typed IRequest object) as any, and there's no way to pass in custom types once, like it is common with other packages. You'd have to explicitly assert/cast the argument type for every function from any to the actual type even though it is already known (I'm running TypeScript and ESLint with very strict settings, so just ignoring the types is not an option). I ended up having to patch your TS declarations file to make it work.

Here is the diff that solved my problem:

diff --git a/node_modules/itty-router/Router.d.ts b/node_modules/itty-router/Router.d.ts
index d5b1d9a..f571055 100644
--- a/node_modules/itty-router/Router.d.ts
+++ b/node_modules/itty-router/Router.d.ts
@@ -16,27 +16,27 @@ export declare type IRequest = {
     };
     proxy?: any;
 } & GenericTraps;
-export interface RouterOptions {
+export interface RouterOptions<TArgs> {
     base?: string;
-    routes?: RouteEntry[];
+    routes?: RouteEntry<TArgs>[];
 }
-export interface RouteHandler {
-    (request: IRequest, ...args: any): any;
+export interface RouteHandler<TArgs> {
+    (request: IRequest, ...args: TArgs): any;
 }
-export declare type RouteEntry = [string, RegExp, RouteHandler[]];
-export declare type Route = <T extends RouterType>(path: string, ...handlers: RouteHandler[]) => T;
-export declare type RouterHints = {
-    all: Route;
-    delete: Route;
-    get: Route;
-    options: Route;
-    patch: Route;
-    post: Route;
-    put: Route;
+export declare type RouteEntry<TArgs> = [string, RegExp, RouteHandler<TArgs>[]];
+export declare type Route<TArgs> = <T extends RouterType<TArgs>>(path: string, ...handlers: RouteHandler<TArgs>[]) => T;
+export declare type RouterHints<TArgs> = {
+    all: Route<TArgs>;
+    delete: Route<TArgs>;
+    get: Route<TArgs>;
+    options: Route<TArgs>;
+    patch: Route<TArgs>;
+    post: Route<TArgs>;
+    put: Route<TArgs>;
 };
-export declare type RouterType = {
-    __proto__: RouterType;
-    routes: RouteEntry[];
-    handle: (request: RequestLike, ...extra: any) => Promise<any>;
-} & RouterHints;
-export declare const Router: ({ base, routes }?: RouterOptions) => RouterType;
+export declare type RouterType<TArgs> = {
+    __proto__: RouterType<TArgs>;
+    routes: RouteEntry<TArgs>[];
+    handle: (request: RequestLike, ...extra: TArgs) => Promise<any>;
+} & RouterHints<TArgs>;
+export declare const Router: <TArgs>({ base, routes }?: RouterOptions<TArgs>) => RouterType<TArgs>;

Here a code snippet:

import { Router, error, json, type RequestLike, withContent, status } from "itty-router";
import { getPosthog } from "./utils";

type Environment = {
    dev: boolean;
};
type Context = ExecutionContext & { posthog: ReturnType<typeof getPosthog> };

const router = Router<[Environment, Context]>();

router
    /* Index route (currently health test) */
    .get("/", (_req, _env, { posthog }) => {
        posthog.capture({
            distinctId: "handler",
            event: "index route requested",
        });
        return {
            ping: "pong",
        };
    })
    /* Webhook subscription for new events */
    .post("/hook", withContent, () => {
        return status(200);
    })
    /* 404 the rest */
    .all("*", () => error(404));

export const handler: ExportedHandler<Record<string, string>> = {
    fetch: (req, env, ctx) => {
        const dev = env["ENVIRONMENT"] === "development";
        const posthog = getPosthog();
        const environment: Environment = { ...env, dev };
        const context: Context = { ...ctx, posthog };

        return router
            .handle(req as RequestLike, environment, context)
            .then(json)
            .catch(error);
    },
};
kwhitley commented 1 year ago

I've got you covered in next/50 :)

Check out the massive changes here: https://itty.dev/itty-router/typescript