Skip to content

Instantly share code, notes, and snippets.

@Gipetto
Created January 20, 2026 21:26
Show Gist options
  • Select an option

  • Save Gipetto/4d483cde4d27ed98f27a66e7bdb2a182 to your computer and use it in GitHub Desktop.

Select an option

Save Gipetto/4d483cde4d27ed98f27a66e7bdb2a182 to your computer and use it in GitHub Desktop.
Tagged Template Literals exercise
// 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