Kodokon kodokon.com

Template literal types: strings computed at compile time

Leverage template literal types to generate unions through Cartesian products and parse string literals with infer.

9 min · 3 questions

Open this lesson in Kodokon

Template literal types bring template literal syntax into the world of types. Interpolate a union and the compiler computes the Cartesian product: every possible combination becomes a member of the result. This is the tool that lets you type event keys, routes, or design-system tokens down to the exact character.

TYPESCRIPT
type Lang = "fr" | "en";
type Theme = "light" | "dark";

type ThemeKey = `${Lang}-${Theme}`;
// "fr-light" | "fr-dark" | "en-light" | "en-dark"
Two interpolated unions: a Cartesian product of 4 members.

The real power shows up when combined with infer: a template literal type becomes a matching pattern over strings. The compiler slices the input literal to match each segment; when several splits are possible, the first occurrence of the separator wins. Add recursion and you get genuine parsers evaluated at compile time.

TYPESCRIPT
type Split<S extends string, Sep extends string> =
  S extends `${infer Head}${Sep}${infer Rest}`
    ? [Head, ...Split<Rest, Sep>]
    : [S];

type Parts = Split<"a.b.c", ".">;
// ["a", "b", "c"]
A fully static recursive split.

This pattern shines when extracting the parameters of an Express-style route. Each recursive call consumes one :param segment, and the final union lists every parameter name, which you can then feed into a Record to type the handler's signature.

TYPESCRIPT
type Params<Path extends string> =
  Path extends `${string}:${infer P}/${infer Rest}`
    ? P | Params<Rest>
    : Path extends `${string}:${infer P}`
      ? P
      : never;

type RouteParams = Params<"/users/:id/posts/:postId">;
// "id" | "postId"
Extracting route parameters at compile time.

TypeScript ships four intrinsic utilities - Uppercase, Lowercase, Capitalize, Uncapitalize - implemented directly inside the compiler (the intrinsic keyword), so you cannot rewrite them yourself. Another production pattern: string & {}. In a union, string would absorb the literals and the editor would lose autocompletion; intersecting with {} creates an equivalent but distinct type that the compiler does not reduce.

TYPESCRIPT
type Method = "get" | "post";
type Handler = `on${Capitalize<Method>}`;
// "onGet" | "onPost"

type KnownColor = "red" | "green" | "blue";
type Color = KnownColor | (string & {});

declare function paint(color: Color): void;

paint("red");     // suggested by autocompletion
paint("#ff8800"); // also accepted
Intrinsics and the string & {} trick for autocompletion.

Knowledge check

Make sure you remember the key points of this lesson.

  1. If A has 3 members and B has 4, how many members does ${A}-${B} contain?
    • 7
    • 12
    • 1
    • 4
  2. What does Capitalize produce when applied to the literal "hello world"?
    • "Hello World"
    • "Hello world"
    • "HELLO WORLD"
  3. Why write KnownColor | (string & {}) rather than KnownColor | string?
    • To forbid strings outside KnownColor
    • To prevent string from absorbing the literals and keep autocompletion
    • To improve compiler performance
    • The two forms are strictly equivalent