Template literal types

Builds string literal types by interpolation, expanding unions into every combination.

Signature
`text${Type}text${Type}`
  • Added in TypeScript 4.1

Parameters

ParameterTypeDescription
$TypetypeThe interpolated type. String, number, bigint, boolean, null and undefined literal types are stringified; string produces an open-ended pattern; a union expands into a cross product.

Return value

A string literal type, or a union of them when any interpolated position is a unionThe concatenated string literal type. Interpolating a union produces the cross product of every combination; interpolating string produces a pattern type that matches any string with that shape.

Errors

Interpolating an object or symbol type is an error. Very large cross products report "Expression produces a union type that is too complex to represent"; the union size limit is 100,000 members.

Changelog

  • PHP 4.1Introduced along with the Uppercase, Lowercase, Capitalize and Uncapitalize intrinsic types and key remapping in mapped types.
  • PHP 4.3Template literal types can be used to infer substrings, so a conditional can pull `${infer Head}-${infer Rest}` apart.
  • PHP 4.8Inference from template literal patterns improved, capturing literal number and bigint types where previously it widened to string.

Template literal types use the backtick syntax in a type position. With concrete literals they simply concatenate: \`hello ${World}\` is "hello world". The interesting behaviour starts when an interpolated position holds a union — the type expands into every combination, and multiple interpolations multiply.

When to reach for it

Whenever a string in your API has structure the compiler could be checking. CSS-style values (\`${number}px\`), route paths, i18n keys, alignment pairs like "top-left", and above all event names. Combined with Capitalize and key remapping, one field list generates onNameChanged, onAgeChanged and nothing else, so a typo is a compile error rather than a handler that silently never fires.

What trips people up

The cross product is real and it multiplies. Three unions of ten members each is a thousand-member union; push past roughly 100,000 and the compiler refuses with "union type that is too complex to represent", and long before that your editor gets slow. Keep at most one or two open dimensions.

Also, the case intrinsics run JavaScript's toUpperCase and toLowerCase directly and are not locale-aware, so Turkish dotted-i and similar cases will not round-trip.

Example

Example · typescript
type World = "world";
type Greeting = `hello ${World}`;  // "hello world"

type Lang = "en" | "fr";
type Page = "home" | "about";
type RouteKey = `${Lang}/${Page}`;
// "en/home" | "en/about" | "fr/home" | "fr/about"

type Field = "name" | "age";
type ChangeEvent = `${Field}Changed`;          // "nameChanged" | "ageChanged"
type Handler = `on${Capitalize<ChangeEvent>}`; // "onNameChanged" | "onAgeChanged"

type Px = `${number}px`;
const width: Px = "320px";  // ok
// const bad: Px = "320";   // error: not assignable to `${number}px`

When to use it

  • Type a setAlignment argument as `${VerticalAlignment}-${HorizontalAlignment}`
  • Derive on*Changed handler names from a model's field list
  • Constrain a CSS size prop to `${number}px` or `${number}rem`

Frequently asked questions

How do I make a type that only accepts strings ending in px?

Write `${number}px` as the type. Interpolating number rather than a literal produces an open pattern, so "320px" and "0px" are accepted while "320" and "320em" are rejected.

Why does my template literal type explode into thousands of members?

Because interpolated unions cross-multiply - two unions of 50 members produce 2,500 strings. The compiler caps unions at 100,000 members and errors past that, so restructure to interpolate a plain string pattern instead of a large union when you do not need every value enumerated.

Are Uppercase and Capitalize locale-aware?

No. They call JavaScript's toUpperCase, toLowerCase and charAt directly with no locale handling, so characters like the Turkish dotless i will not transform the way a locale-aware routine would.

More examples

The handbook's locale ID cross product

Each interpolation multiplies rather than adds: four locale IDs times three languages is twelve valid strings, all enumerated by the compiler. setAlignment is the payoff - nine legal values are described by one line instead of a nine-member hand-written union, and a misspelling is rejected at the call site.

Example · typescript
type EmailLocaleIDs = "welcome_email" | "email_heading";
type FooterLocaleIDs = "footer_title" | "footer_sendoff";

type AllLocaleIDs = `${EmailLocaleIDs | FooterLocaleIDs}_id`;
// "welcome_email_id" | "email_heading_id" | "footer_title_id" | "footer_sendoff_id"

type Lang = "en" | "ja" | "pt";
type LocaleMessageIDs = `${Lang}_${AllLocaleIDs}`;
// 3 x 4 = 12 members, from "en_welcome_email_id" to "pt_footer_sendoff_id"

type VerticalAlignment = "top" | "middle" | "bottom";
type HorizontalAlignment = "left" | "center" | "right";

declare function setAlignment(
  value: `${VerticalAlignment}-${HorizontalAlignment}`
): void;

setAlignment("top-left");    // works
// setAlignment("top-middel"); // error, typo caught

Case intrinsics and pulling strings apart with infer

The four intrinsics are compiler built-ins that call the corresponding JavaScript string methods, so they are fast but not locale-aware. Split shows the reverse direction: a template pattern in an extends clause matches a string literal and infer captures the pieces, recursing to build a tuple. This is how routing and query-string libraries derive parameter names from a path string.

Example · typescript
type Greeting = "Hello, world";
type Shouty = Uppercase<Greeting>;      // "HELLO, WORLD"
type Quiet = Lowercase<Greeting>;       // "hello, world"
type Upper = Capitalize<"hello there">; // "Hello there"
type Lower = Uncapitalize<"HELLO">;     // "hELLO"

type ASCIICacheKey<Str extends string> = `ID-${Uppercase<Str>}`;
type MainID = ASCIICacheKey<"my_app">;  // "ID-MY_APP"

// TypeScript 4.3+: infer against a template pattern
type Split<S extends string> =
  S extends `${infer Head}-${infer Rest}` ? [Head, ...Split<Rest>] : [S];

type Parts = Split<"a-b-c">; // ["a", "b", "c"]

type Method<S> = S extends `${infer M} ${string}` ? M : never;
type M = Method<"GET /users">; // "GET"

Discussion

  • Be the first to comment on this lesson.