TypeScript Template Literal Types: Typed Route Params from "/tasks/:id"
November 6, 2026
Welcome to episode 43 of TypeScript from Zero, a series for complete beginners (no JavaScript needed). Welcome back to TypeScript from zero! You know template literals from episode four: text with dollar and curly braces. TypeScript has the same thing for types. Today we use it to type routes, so that a path like slash tasks slash colon id gives a handler with an id. A new file called routes.ts in the tasks project. Every code block below is the exact code and output from the video, ready to copy.
- All episodes are in the TypeScript from Zero playlist.
- Versions used: Node.js 26.13.0, npm 11.20.0, TypeScript 7.0.2.
Setup
We start in the terminal and open the project in VS Code:
cd tasks
touch src/routes.ts
code .1. Basics
A template literal type builds text types. Here on plus the capitalized words click and hover. That makes two types: on Click and on Hover. If we write on Key, tsc shows us the two that are allowed. TS2322.
src/routes.ts:
type EventName = `on${Capitalize<"click" | "hover">}`;
const h: EventName = "onKey";$ npx tsc --noEmit
src/routes.ts:2:7 - error TS2322: Type '"onKey"' is not assignable to type '"onClick" | "onHover"'.
2 const h: EventName = "onKey";
~
Found 1 error in src/routes.ts:22. Fix
We write on Click, and it is accepted.
src/routes.ts:
type EventName = `on${Capitalize<"click" | "hover">}`;
const h: EventName = "onClick";$ npx tsc --noEmit3. Params type
Now the clever part. Params takes a path as a text type. Does it contain a colon, a name, and a slash, and more? Then the answer is that name, together with the params of the rest of the path. Does it end with a colon and a name? Then the answer is that name. Otherwise, nothing. Infer captures the names from the pattern. Route params turns the names into an object type: every name is a key, with a string value.
src/routes.ts:
type EventName = `on${Capitalize<"click" | "hover">}`;
const h: EventName = "onClick";
type Params<S extends string> =
S extends `${string}:${infer P}/${infer Rest}` ? P | Params<`/${Rest}`>
: S extends `${string}:${infer P}` ? P
: never;
type RouteParams<S extends string> = { [K in Params<S>]: string };$ npx tsc --noEmit4. route
The route function takes the path and a handler. The type of the path is kept exactly, as S, so the handler parameter is typed from it. At runtime we only collect the parts that start with a colon. For the path with id, the handler gets an object with id. For the longer path, user id and task id. tsc knows both shapes.
src/routes.ts:
type EventName = `on${Capitalize<"click" | "hover">}`;
const h: EventName = "onClick";
type Params<S extends string> =
S extends `${string}:${infer P}/${infer Rest}` ? P | Params<`/${Rest}`>
: S extends `${string}:${infer P}` ? P
: never;
type RouteParams<S extends string> = { [K in Params<S>]: string };
function route<S extends string>(path: S, handler: (params: RouteParams<S>) => void): void {
const params: Record<string, string> = {};
for (const part of path.split("/")) {
if (part.startsWith(":")) {
params[part.slice(1)] = "demo-" + part.slice(1);
}
}
handler(params as RouteParams<S>);
}
route("/tasks/:id", (p) => console.log(p.id));
route("/users/:userId/tasks/:taskId", (p) => console.log(p.userId, p.taskId));$ node src/routes.ts
demo-id
demo-userId demo-taskId5. Error
And now the payoff. In the handler, we ask for a name, which the path does not have. tsc says: property name does not exist on type with just id. TS2339. A typo in a route parameter is caught while we type.
src/routes.ts:
type EventName = `on${Capitalize<"click" | "hover">}`;
const h: EventName = "onClick";
type Params<S extends string> =
S extends `${string}:${infer P}/${infer Rest}` ? P | Params<`/${Rest}`>
: S extends `${string}:${infer P}` ? P
: never;
type RouteParams<S extends string> = { [K in Params<S>]: string };
function route<S extends string>(path: S, handler: (params: RouteParams<S>) => void): void {
const params: Record<string, string> = {};
for (const part of path.split("/")) {
if (part.startsWith(":")) {
params[part.slice(1)] = "demo-" + part.slice(1);
}
}
handler(params as RouteParams<S>);
}
route("/tasks/:id", (p) => console.log(p.id));
route("/users/:userId/tasks/:taskId", (p) => console.log(p.userId, p.taskId));
route("/tasks/:id", (p) => console.log(p.name));$ npx tsc --noEmit
src/routes.ts:21:42 - error TS2339: Property 'name' does not exist on type 'RouteParams<"/tasks/:id">'.
21 route("/tasks/:id", (p) => console.log(p.name));
~~~~
Found 1 error in src/routes.ts:216. Fix
We delete the line, and everything is quiet.
src/routes.ts:
type EventName = `on${Capitalize<"click" | "hover">}`;
const h: EventName = "onClick";
type Params<S extends string> =
S extends `${string}:${infer P}/${infer Rest}` ? P | Params<`/${Rest}`>
: S extends `${string}:${infer P}` ? P
: never;
type RouteParams<S extends string> = { [K in Params<S>]: string };
function route<S extends string>(path: S, handler: (params: RouteParams<S>) => void): void {
const params: Record<string, string> = {};
for (const part of path.split("/")) {
if (part.startsWith(":")) {
params[part.slice(1)] = "demo-" + part.slice(1);
}
}
handler(params as RouteParams<S>);
}
route("/tasks/:id", (p) => console.log(p.id));
route("/users/:userId/tasks/:taskId", (p) => console.log(p.userId, p.taskId));$ npx tsc --noEmitRecap
Today: template literal types, built from other text types, matching a pattern with infer, and typed route parameters. Next episode: function overloads and the this type.