Real World Example

A Type-safe Mini Router

Next.js typed routes, Hono and tRPC give you autocomplete and errors for URLs. This mini router types the handler params from the route pattern, like Express with typed paths.

Example

type ParamNames<P extends string> =
  P extends `${string}:${infer Name}/${infer Rest}` ? Name | ParamNames<Rest>
  : P extends `${string}:${infer Name}` ? Name : never;
type Handler<P extends string> = (params: { [K in ParamNames<P>]: string }) => string;

class Router {
  private routes: { regex: RegExp; keys: string[]; handler: (p: Record<string, string>) => string }[] = [];

  get<P extends string>(pattern: P, handler: Handler<P>): this {
    const keys = [...pattern.matchAll(/:(\w+)/g)].map((m) => m[1]);
    const regex = new RegExp("^" + pattern.replace(/:\w+/g, "([^/]+)") + "$");
    this.routes.push({ regex, keys, handler: handler as (p: Record<string, string>) => string });
    return this;
  }

  handle(url: string): string {
    for (const r of this.routes) {
      const m = url.match(r.regex);
      if (m) return r.handler(Object.fromEntries(r.keys.map((k, i) => [k, m[i + 1]])));
    }
    return "404 Not Found";
  }
}

const router = new Router()
  .get("/products/:id", ({ id }) => `Product page for ${id}`)
  .get("/users/:userId/orders/:orderId", ({ userId, orderId }) => `Order ${orderId} of ${userId}`);

["/products/42", "/users/u7/orders/OD-9", "/cart"].forEach((u) => console.log(u.padEnd(22), "->", router.handle(u)));

Output

/products/42           -> Product page for 42
/users/u7/orders/OD-9  -> Order OD-9 of u7
/cart                  -> 404 Not Found