Created
January 20, 2026 21:26
-
-
Save Gipetto/4d483cde4d27ed98f27a66e7bdb2a182 to your computer and use it in GitHub Desktop.
Tagged Template Literals exercise
This file contains hidden or 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
| // Define our route param descriptors. | |
| // This allows us to type the route params. | |
| type ParamDescriptor<A> = { | |
| name: string; | |
| parse: (raw: string) => A; | |
| }; | |
| const int = (name: string): ParamDescriptor<number> => { | |
| return { | |
| name, | |
| parse(raw) { | |
| const n = Number(raw); | |
| if (!Number.isInteger(n)) { | |
| throw new Error(`Param "${name}" must be an integer, got "${raw}"`); | |
| } | |
| return n; | |
| }, | |
| }; | |
| } | |
| const str = (name: string): ParamDescriptor<string> => { | |
| return { | |
| name, | |
| parse(raw) { | |
| return raw; | |
| }, | |
| }; | |
| } | |
| type Segment<P extends readonly ParamDescriptor<unknown>[]> = | |
| | { kind: "literal"; value: string } | |
| | { kind: "param"; descriptor: P[number] } | |
| type Route<P extends readonly ParamDescriptor<unknown>[]> = { | |
| namespace: string; | |
| path: string; | |
| segments: Segment<P>[]; | |
| }; | |
| const parsePath = (namespace: string) => { | |
| return <const P extends readonly ParamDescriptor<unknown>[]>( | |
| strings: TemplateStringsArray, | |
| ...params: P | |
| ): Route<P> => { | |
| const segments: Segment<P>[] = [] | |
| const path = strings.reduce((acc, s, i) => { | |
| if (s !== "") { | |
| const parts = s.split("/").filter((x) => x !== "") | |
| for (const p in parts) { | |
| segments.push({ | |
| kind: "literal", | |
| value: parts[p] | |
| }) | |
| } | |
| } | |
| acc += s | |
| if (i < params.length) { | |
| segments.push({ | |
| kind: "param", | |
| descriptor: params[i] | |
| }) | |
| acc += `:${params[i].name}` | |
| } | |
| return acc | |
| }, "") | |
| return { | |
| namespace, | |
| path, | |
| segments | |
| } | |
| } | |
| } | |
| type ParamsOf<P extends readonly ParamDescriptor<unknown>[]> = { | |
| [K in P[number] as K["name"]]: ReturnType<K["parse"]>; | |
| } & { | |
| namespace: string | |
| path: string | |
| } | |
| const matchPath = <P extends readonly ParamDescriptor<unknown>[]>( | |
| route: Route<P>, | |
| url: string | |
| ): ParamsOf<P> | null => { | |
| const urlParts = url.split("/").filter(Boolean) | |
| if (urlParts.length !== route.segments.length) { | |
| return null | |
| } | |
| const match: Record<string, unknown> = { | |
| namespace: route.namespace, | |
| path: route.path | |
| } | |
| for (let i = 0; i < route.segments.length; i++) { | |
| const segment = route.segments[i] | |
| const part = urlParts[i] | |
| if (segment.kind === "literal") { | |
| if (segment.value !== part) { | |
| return null | |
| } | |
| continue | |
| } | |
| const d = segment.descriptor | |
| match[d.name] = d.parse(part) | |
| } | |
| return match as ParamsOf<P> | |
| } | |
| const r = parsePath("foo")`/path/to/${int("id")}/edit/${str("key")}` | |
| console.log(r) | |
| /* | |
| { | |
| namespace: 'foo', | |
| path: '/path/to/:id/edit/:key', | |
| segments: [ | |
| { kind: 'literal', value: 'path' }, | |
| { kind: 'literal', value: 'to' }, | |
| { kind: 'param', descriptor: [Object] }, | |
| { kind: 'literal', value: 'edit' }, | |
| { kind: 'param', descriptor: [Object] } | |
| ] | |
| } | |
| */ | |
| const m = matchPath(r, "/path/to/123/edit/foo") | |
| console.log(m) | |
| /* | |
| { | |
| namespace: 'foo', | |
| path: '/path/to/:id/edit/:key', | |
| id: 123, | |
| key: 'foo' | |
| } | |
| */ |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment