Template literal types

TypeScript · Advanced · 12 min

They type string-based APIs precisely: event names, CSS units, route parameters, i18n keys and more.

Template literal types use the same backtick syntax as JavaScript template strings, but at the type level:

type Greeting = `hello ${string}`;   // any string starting with "hello "
type Px = `${number}px`;              // "12px", "1.5px", ...

Placeholders can hold string, number, bigint, boolean, literal types and unions. With unions they produce the cross product:

type Size = "sm" | "lg";
type Color = "red" | "blue";
type ClassName = `${Size}-${Color}`; // "sm-red" | "sm-blue" | "lg-red" | "lg-blue"

Be careful: large unions multiply quickly, and the compiler limits a union to around 100,000 members.

The intrinsic helpers Uppercase, Lowercase, Capitalize and Uncapitalize transform literal types. Combined with mapped-type key remapping they generate APIs such as onClick/onChange handler names from a list of events.

Template literal types also parse strings in conditional types using infer:

type Param<S> = S extends `${string}:${infer P}/${infer Rest}`
  ? P | Param<`/${Rest}`>
  : S extends `${string}:${infer P}` ? P : never;
// Param<"/users/:id/posts/:postId"> is "id" | "postId"

That's how routers type "/users/:id/posts/:postId" into { id: string; postId: string }. A placeholder before a fixed delimiter matches as little as possible (up to the first delimiter), which makes left-to-right parsing predictable.

Use these where strings really are structured. A pattern like ` ${number}px gives callers autocomplete and catches "12pz", but a value typed string` (e.g. from user input) isn't assignable to it without a check — so keep runtime validation at the boundary.

Example

type Size = "sm" | "lg";
type Tone = "primary" | "danger";
type ButtonClass = `btn-${Size}-${Tone}`;   // 4 combinations

type Events = "click" | "focus";
type Handlers = { [E in Events as `on${Capitalize<E>}`]: (e: E) => void };

type RouteParams<S extends string> =
  S extends `${string}:${infer P}/${infer Rest}`
    ? { [K in P | keyof RouteParams<`/${Rest}`>]: string }
    : S extends `${string}:${infer P}` ? { [K in P]: string } : {};

type Params = RouteParams<"/users/:userId/posts/:postId">;
// { userId: string; postId: string }

const cls: ButtonClass = "btn-lg-danger";
const handlers: Handlers = { onClick: e => console.log(e), onFocus: e => console.log(e) };
const params: Params = { userId: "7", postId: "42" };
const width: `${number}px` = "12.5px";
// @ts-expect-error -- "12pz" doesn't match `${number}px`
const bad: `${number}px` = "12pz";
handlers.onClick("click");
console.log(cls, params.postId, width);

Remember

  • Backtick types build string literal types; unions expand to all combinations
  • Capitalize & co. transform literals; pair them with key remapping
  • infer inside template types parses strings at the type level

Watch out for

  • Creating huge unions from cross products of large unions, which slows the checker or exceeds its limits.

Check yourself

  1. How many members does `${"a" | "b"}-${1 | 2 | 3}` have?
  2. Is a value of type string assignable to `${number}px`?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: www.typescriptlang.org/docs/handbook/2/template-literal-types.html