typestar

typed-router.ts en TypeScript

Un router cuyos parámetros de ruta se extraen del patrón en tiempo de compilación.

type Segments<Path extends string> =
  Path extends `${infer Head}/${infer Rest}`
    ? Head | Segments<Rest>
    : Path;

type ParamName<Segment extends string> =
  Segment extends `{${infer Name}}` ? Name : never;

type Params<Path extends string> = {
  [K in ParamName<Segments<Path>>]: string;
};

type Method = "GET" | "POST" | "DELETE";

interface Request<Path extends string> {
  method: Method;
  path: string;
  params: Params<Path>;
}

type Handler<Path extends string, Result> =
  (request: Request<Path>) => Result | Promise<Result>;

interface Route {
  method: Method;
  pattern: string;
  handler: Handler<string, unknown>;
}

class Router {
  #routes: Route[] = [];

  on<Path extends string, Result>(
    method: Method,
    pattern: Path,
    handler: Handler<Path, Result>,
  ): this {
    this.#routes.push({
      method,
      pattern,
      handler: handler as Handler<string, unknown>,
    });
    return this;
  }

  async handle(method: Method, path: string): Promise<unknown> {
    for (const route of this.#routes) {
      if (route.method !== method) continue;
      const params = match(route.pattern, path);
      if (!params) continue;
      return route.handler({ method, path, params });
    }
    return { status: 404 };
  }
}

function match(pattern: string, path: string): Record<string, string> | null {
  const wanted = pattern.split("/");
  const given = path.split("/");
  if (wanted.length !== given.length) return null;

  const params: Record<string, string> = {};
  for (const [index, part] of wanted.entries()) {
    const actual = given[index] ?? "";
    if (part.startsWith("{") && part.endsWith("}")) {
      params[part.slice(1, -1)] = actual;
    } else if (part !== actual) {
      return null;
    }
  }
  return params;
}

const router = new Router()
  .on("GET", "/healthz", () => ({ status: "ok" }))
  .on("GET", "/tours/{lang}/{tour}", ({ params }) => ({
    lang: params.lang,
    tour: params.tour,
  }))
  .on("DELETE", "/tours/{lang}", ({ params }) => ({ deleted: params.lang }));

// .on("GET", "/tours/{lang}", ({ params }) => params.slug) // param no existe

const results = await Promise.all([
  router.handle("GET", "/healthz"),
  router.handle("GET", "/tours/ts/generics"),
  router.handle("DELETE", "/tours/go"),
  router.handle("GET", "/nope"),
]);
console.log(results);

Cómo funciona

  1. Un tipo de literal de plantilla parsea el patrón a un objeto de parámetros.
  2. El argumento params del handler se tipa desde la ruta que registra.
  3. Un nombre de parámetro equivocado no compila, en vez de fallar al ejecutar.

Palabras clave y builtins usados aquí

El intento, en números

Líneas
90
Caracteres a escribir
2155
Tokens
581
Ritmo de tres estrellas
115 tpm

Al ritmo de tres estrellas de 115 tokens por minuto, este intento toma unos 303 segundos.

Escribe este fragmento

Paso 1 de 1 en Bis; paso 12 de 12 en Programación a nivel de tipos.

← Anterior