M Logo
๐Ÿ“ TypeScript from Zero ยท Part 43 of 50

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.

Setup

We start in the terminal and open the project in VS Code:

bash
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:

ts
type EventName = `on${Capitalize<"click" | "hover">}`;
const h: EventName = "onKey";
text
$ 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:2

2. Fix

We write on Click, and it is accepted.

src/routes.ts:

ts
type EventName = `on${Capitalize<"click" | "hover">}`;
const h: EventName = "onClick";
bash
$ npx tsc --noEmit

3. 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:

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 };
bash
$ npx tsc --noEmit

4. 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:

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));
text
$ node src/routes.ts
demo-id
demo-userId demo-taskId

5. 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:

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));
text
$ 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:21

6. Fix

We delete the line, and everything is quiet.

src/routes.ts:

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));
bash
$ npx tsc --noEmit

Recap

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.

ยฉ 2026 All Rights Reserved By Mahmudul Alam