Skip to content

Template Literal Types

Template literal types look like JavaScript template strings, but they build types. You interpolate other types into a string-shaped type:

type Greeting = `Hello, ${string}`;
const a: Greeting = "Hello, Ada"; // ✅
const b: Greeting = "Hi, Ada"; // ❌ must start with "Hello, "

Interpolate a literal union and the template distributes over it — producing every combination as a union of literal types:

type Lang = "en" | "th";
type Kind = "page" | "post";
type Key = `${Lang}-${Kind}`;
// ^? "en-page" | "en-post" | "th-page" | "th-post"

Two unions of two members give four literal types. This is how libraries generate exact string keys — CSS properties, event names, i18n keys — with zero runtime code.

TypeScript ships four built-in types that transform string literal types: Uppercase, Lowercase, Capitalize, and Uncapitalize.

type Loud = Uppercase<"hello">; // "HELLO"
type Cap = Capitalize<"name">; // "Name"
// Combine with a template to build event handler names
type Handler<E extends string> = `on${Capitalize<E>}`;
type ClickHandler = Handler<"click">; // "onClick"
flowchart LR
  u["union: en or th"] -->|"interpolate into 'X-page'"| out["en-page or th-page"]
A union distributes across a template literal type

The real power arrives when you combine template literals with a conditional type and infer: you can destructure a string type, capturing part of it into a new type variable.

// Capture the event name after the "on" prefix
type EventName<T> = T extends `on${infer E}` ? E : never;
type E1 = EventName<"onClick">; // "Click"
type E2 = EventName<"onHover">; // "Hover"
type E3 = EventName<"click">; // never — no "on" prefix
// Split a route into its parts
type Method<T> = T extends `${infer M} ${string}` ? M : never;
type M = Method<"GET /users">; // "GET"

infer E says “match anything here and call it E.” This is genuine parsing at the type level — the technique behind typed routers that extract :id params from a path string and libraries that validate format strings.

  • Typed keys: `${Theme}-${Shade}` for a design-token type.
  • Event maps: turning "click" into "onClick" for prop types.
  • Route params: extracting id from "/users/:id" into { id: string }.
  • ORM/query builders: parsing a column string into a typed shape.

Use it where a string genuinely encodes structure. Reach for it too eagerly and you get types no teammate can read — the recurring trade-off of type-level programming.

What is the type `${Lang}-${Kind}` where `Lang = "en" | "th"` and `Kind = "a" | "b"`?
What does `Capitalize<"click">` produce?
In `T extends \`on${infer E}\` ? E : never`, what is `infer E` doing?
What does `EventName<"click">` return if `EventName<T> = T extends \`on${infer E}\` ? E : never`?