Last active
June 29, 2024 04:51
-
-
Save fsubal/7af7b36a854cfb9fdf778e2d6bef8fe5 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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