Template Literal Types in TypeScript

1. Syntax and basics

Template literal types use the same syntax as JS template literals, but with types interpolated instead of values. The result is a string literal type computed from the constituent parts.

types.ts
type World = "world"; type Greeting = `hello ${World}`; // type Greeting = "hello world" type Color = "red" | "green" | "blue"; type ColorVar = `--color-${Color}`; // "--color-red" | "--color-green" | "--color-blue"

Interpolated positions accept any type with a string representation: string, number, bigint, boolean, null, undefined, or unions thereof. Other types (objects, arrays) are rejected at the type position.

type Px = `${number}px`;
type Flag = `is-${boolean}`;
// "is-true" | "is-false" (boolean expands to its union)

const a: Px = "10px";   // OK
const b: Px = "10em";   // Error
Note A template literal type with a non-literal placeholder (${number}, ${string}) is a pattern type. It matches the infinite set of strings conforming to that pattern and is used both for validation and for assignability checks.

2. Union distribution

When a union type is interpolated, TypeScript distributes over each member, producing the cartesian product of all union combinations across all interpolation slots.

type Direction = "top" | "right" | "bottom" | "left";
type Size = "sm" | "md" | "lg";

type Spacing = `m${Direction}-${Size}`;
// "mtop-sm" | "mtop-md" | "mtop-lg" | "mright-sm" | ... (12 members total)
Warn Distribution is multiplicative. Two unions of size 4 and 3 in the same template produce 12 members; three unions of size 10 produce 1000. Large unions in template literal types can blow up compiler memory and slow type-checking significantly. Keep input unions small or generate the type programmatically and assert it.

3. Intrinsic string manipulation types

TypeScript ships four built-in generic types for case transformation, implemented natively by the compiler (not expressible in user-land TS).

TypeEffectExample
Uppercase<S>Converts every character to uppercaseUppercase<"abc"> // "ABC"
Lowercase<S>Converts every character to lowercaseLowercase<"ABC"> // "abc"
Capitalize<S>Uppercases the first character onlyCapitalize<"abc"> // "Abc"
Uncapitalize<S>Lowercases the first character onlyUncapitalize<"Abc"> // "abc"

These compose naturally with template literal types:

type EventName<T extends string> = `on${Capitalize}`;

type ClickEvent = EventName<"click">;   // "onClick"
type FocusEvent = EventName<"focus">;   // "onFocus"

4. Inferring substrings with infer

Template literal types can appear in conditional types, where each interpolation slot becomes an inference site via infer. This enables pattern matching and decomposition of string literal types.

type ParseRoute<T extends string> =
  T extends `${infer Segment}/${infer Rest}`
    ? [Segment, ...ParseRoute<Rest>]
    : [T];

type Parts = ParseRoute<"users/:id/posts">;
// ["users", ":id", "posts"]

The same mechanism is used to extract route parameters, a common pattern in typed routing libraries:

type ExtractParams<T extends string> =
  T extends `${string}:${infer Param}/${infer Rest}`
    ? Param | ExtractParams<Rest>
    : T extends `${string}:${infer Param}`
      ? Param
      : never;

type Params = ExtractParams<"/users/:userId/posts/:postId">;
// "userId" | "postId"
Tip Recursive conditional types like the above are evaluated eagerly by the compiler up to a depth limit (historically ~1000, tail-recursion-optimized in TS 4.5+ for certain patterns). For very long strings, prefer tail-recursive accumulator patterns to avoid hitting "Type instantiation is excessively deep and possibly infinite".

5. Combining with mapped types

Template literal types as computed property names ("key remapping") let you derive new object shapes from existing ones — the basis for typed event emitters, getters/setters, and action creators.

type Getters<T> = {
  [K in keyof T as `get${Capitalize}`]: () => T[K]
};

interface Person {
  name: string;
  age: number;
}

type PersonGetters = Getters<Person>;
// { getName: () => string; getAge: () => number }

The string & K intersection is required because K (from keyof T) may include symbol or number, which are not valid template literal interpolations on their own without that narrowing.

type EventMap = {
  click: MouseEvent;
  keydown: KeyboardEvent;
};

type Handlers = {
  [K in keyof EventMap as `on${Capitalize}`]?:
    (ev: EventMap[K]) => void
};
// { onClick?: (ev: MouseEvent) => void; onKeydown?: (ev: KeyboardEvent) => void }

6. Practical patterns

CSS property/value pairs

type Unit = "px" | "%" | "rem" | "em";
type CSSValue = `${number}${Unit}` | "0" | "auto";

Typed object paths (dot notation)

type PathsOf<T, Prefix extends string = ""> = {
  [K in keyof T & string]: T[K] extends object
    ? PathsOf<T[K], `${Prefix}${K}.`>
    : `${Prefix}${K}`
}[keyof T & string];

interface Config {
  db: { host: string; port: number };
  debug: boolean;
}

type ConfigPath = PathsOf<Config>;
// "db.host" | "db.port" | "debug"

SQL-like query builder constraints

type SelectQuery<T extends string> =
  T extends `SELECT ${string} FROM ${string}` ? T : never;

function query<T extends string>(sql: SelectQuery<T>): void {}

query("SELECT * FROM users");     // OK
query("DELETE FROM users");     // Error: argument type 'never'

Branded units via template literals

type Meters = `${number}m`;
type Feet = `${number}ft`;

function toMeters(value: Feet): Meters {
  const n = parseFloat(value);
  return `${n * 0.3048}m` as Meters;
}

7. Limitations and gotchas

Danger Avoid template literal types over unions derived from large enums or generated string unions (e.g. all HTML element tag names crossed with all ARIA attributes). The resulting type can have tens of thousands of members, causing IDE responsiveness issues and significant tsc slowdowns. Profile with tsc --extendedDiagnostics if type-checking time regresses after introducing such a type.