Template Literal Types

Advanced
11 min

Template Literal Types

Template literal types bring JavaScript's backtick strings to the type level. They let you describe strings by shape (#${string}), generate unions of names from other unions (on${Capitalize<Event>}), and even parse strings apart with infer. Together with mapped types they power type-safe CSS helpers, event emitters and router libraries. This lesson shows the syntax, the intrinsic string utilities, and the parsing patterns that make these libraries work.

Building Strings From Types

The syntax mirrors runtime template literals, but the placeholders hold types:

typescript
type Lang = "en" | "de"; type Page = "home" | "about"; type RouteKey = `${Lang}/${Page}`; // "en/home" | "en/about" | "de/home" | "de/about" type Greeting = `Hello, ${string}!`; const g: Greeting = "Hello, Ada!"; // OK const h: Greeting = "Hi, Ada!"; // Error

When a placeholder is a union, TypeScript produces the cross product of all members. Primitive types are allowed as placeholders too: string, number, bigint, boolean, null and undefined. `${number}px` matches "12px" but not "12em" or "px".

Unions are expanded eagerly, and the compiler rejects templates that would exceed 100,000 members. Keep the source unions small.

Intrinsic String Manipulation Types

Four built-in types change the case of string literals. They are implemented inside the compiler and work on unions as well:

| Type | "userName" becomes | |---|---| | Uppercase<S> | "USERNAME" | | Lowercase<S> | "username" | | Capitalize<S> | "UserName" | | Uncapitalize<S> | "userName" |

typescript
type Field = "name" | "email"; type Setter = `set${Capitalize<Field>}`; // "setName" | "setEmail"

Typed Event Names With Mapped Types

The classic use case is generating listener methods from a map of events:

typescript
interface Events { connect: { host: string }; message: { body: string }; close: undefined; } type Listeners = { [E in keyof Events as `on${Capitalize<E>}`]: (payload: Events[E]) => void; }; // { onConnect: (payload: { host: string }) => void; onMessage: ...; onClose: ... } function watch<E extends keyof Events>(event: E, handler: (payload: Events[E]) => void) { /* ... */ } watch("message", (p) => p.body); // p: { body: string } watch("open", () => {}); // Error: '"open"' is not assignable

Add a new key to Events and every derived name, handler signature and watch overload updates automatically.

Parsing Strings With infer

Inside a conditional type, infer can capture the part of a string that matches a placeholder. This turns a template into a pattern matcher:

typescript
type Split<S extends string, Sep extends string> = S extends `${infer Head}${Sep}${infer Tail}` ? [Head, ...Split<Tail, Sep>] : [S]; type Parts = Split<"a.b.c", ".">; // ["a", "b", "c"] type Trim<S extends string> = S extends ` ${infer R}` ? Trim<R> : S extends `${infer L} ` ? Trim<L> : S; type Clean = Trim<" hello ">; // "hello"

Each infer placeholder matches the shortest possible string for all but the last position, which is why Split peels off one segment at a time. Matching is by string structure only; there is no regular expression support.

Practical Patterns

typescript
// Strongly typed CSS-like values type Unit = "px" | "rem" | "%"; type Length = `${number}${Unit}`; function setWidth(el: HTMLElement, w: Length) { el.style.width = w; } setWidth(document.body, "100%"); // OK setWidth(document.body, "100"); // Error // Object paths for a config accessor interface Config { db: { host: string; port: number }; debug: boolean } type Path<T, P extends string = ""> = { [K in keyof T & string]: T[K] extends object ? Path<T[K], `${P}${K}.`> : `${P}${K}`; }[keyof T & string]; type ConfigPath = Path<Config>; // "db.host" | "db.port" | "debug"

A get(config, "db.port") helper typed with ConfigPath catches typos in dotted paths at compile time, a pattern used by form libraries and i18n tooling.

Tips

  • Template literal types describe literal strings. A plain string value never satisfies `#${string}` unless narrowed or asserted; validate user input at runtime.
  • Prefer keyof T & string (or string & K) inside templates so number and symbol keys do not cause errors.
  • Recursive parsers hit the recursion limit on long strings; use them for short, structured literals such as routes and paths.
Quick Quiz
Question 1 of 3

What is `` `${"a" | "b"}-${"x" | "y"}` ``?

Key Takeaways

  • Template literal types build string literal types from other types; unions expand to every combination.
  • Placeholders may be string, number and other primitives to describe string shapes such as `${number}px`.
  • Uppercase, Lowercase, Capitalize and Uncapitalize transform literal case and work on unions.
  • With mapped type key remapping they generate event handler names and setters from a single source.
  • infer inside a template pattern parses strings apart, enabling typed routes, paths and splitters.

Next lesson: Modules and Namespaces — organise code with ES modules, type-only imports and the legacy namespace syntax.

Template Literal Types - TypeScript | CodeYourCraft | CodeYourCraft