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
- Un tipo de literal de plantilla parsea el patrón a un objeto de parámetros.
- El argumento params del handler se tipa desde la ruta que registra.
- Un nombre de parámetro equivocado no compila, en vez de fallar al ejecutar.
Palabras clave y builtins usados aquí
HandlerMethodParamsPathPromiseRecordRequestasasyncawaitclassconstcontinueelseextendsforfunctionhandlerifinterfaceneverofreturnstringthistype
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.
Paso 1 de 1 en Bis; paso 12 de 12 en Programación a nivel de tipos.