Skip to content

Instantly share code, notes, and snippets.

@fsubal
Last active June 29, 2024 04:51
Show Gist options
  • Save fsubal/7af7b36a854cfb9fdf778e2d6bef8fe5 to your computer and use it in GitHub Desktop.
Save fsubal/7af7b36a854cfb9fdf778e2d6bef8fe5 to your computer and use it in GitHub Desktop.
type ExtractParams<Template extends string> =
Template extends `${infer A}/${infer B}` ? ExtractParams<A> | ExtractParams<B>
: Template extends `/${infer A}` ? ExtractParams<A>
: Template extends `:${infer A}/${infer B}` ? A | ExtractParams<B>
: Template extends `:${infer A}` ? A
: never
type Params<Template extends string> = ExtractParams<Template> extends string ? Record<ExtractParams<Template>, string> : never
class TrieNode<Template extends `/${string}`, R> {
children: Record<string, TrieNode<any, any>> = {};
handler?: (params: Params<Template>) => R;
constructor(readonly paramName: Template | null) {}
get isDynamic() {
return this.paramName != null;
}
}
interface ResolvedRoute<Template extends `/${string}`, R extends unknown> {
handler(params: Params<Template>): R
params: Params<Template>
}
class Router {
root = new TrieNode<any, any>(null);
add<Template extends `/${string}`, R>(
path: Template,
handler: (params: Params<Template>) => R
) {
let node = this.root;
const parts = path.split("/").filter((part) => part.length > 0);
for (let part of parts) {
let paramName: Template | undefined = undefined;
if (part.startsWith(":")) {
paramName = part.slice(1) as Template;
part = ":";
}
if (!node.children[part]) {
node.children[part] = new TrieNode(paramName!);
}
node = node.children[part];
}
node.handler = handler;
}
lookup<
Template extends `/${string}`,
R extends unknown
>(path: Template): ResolvedRoute<Template, R> | null {
let node = this.root;
const parts = path.split("/").filter((part) => part.length > 0);
const params = {} as Params<Template>;
for (const part of parts) {
if (node.children[part]) {
node = node.children[part];
} else if (node.children[":"]) {
node = node.children[":"];
params[node.paramName!] = part;
} else {
return null;
}
}
return { handler: (node as TrieNode<Template, R>).handler!, params };
}
resolve<Template extends `/${string}`, R extends unknown>(path: Template) {
const route = router.lookup(path);
return route?.handler(route.params) as R;
}
}
// Example usage:
const router = new Router();
router.add("/home", () => {
console.log("Home page handler");
});
router.add("/about", () => {
console.log("About page handler");
});
router.add("/items/:id", (params) => {
console.log(`Item handler for item id: ${params.id}`);
});
const homeRoute = router.lookup("/home");
homeRoute?.handler(homeRoute.params); // Home page handler
const aboutRoute = router.lookup("/about");
aboutRoute?.handler(aboutRoute.params); // About page handler
const itemRoute = router.lookup("/items/123");
itemRoute?.handler(itemRoute.params); // Item handler for item id: 123
const notFoundRoute = router.lookup("/not-found");
notFoundRoute?.handler(notFoundRoute.params); // null
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment