TypeScriptTemplate Literal Types

Template Literal Types

Template literal types (introduced in TypeScript 4.1) bring JavaScript's template literal syntax into the type system. Just as \Hello, ${name}!`` builds a string at runtime, a template literal type builds a string type at compile time.

This allows you to express patterns like "on" + EventName, "--css-var", or "GET /users/:id" directly in the type system — catching mistakes at compile time that would otherwise be runtime errors.

Basic Syntax

The syntax mirrors JavaScript template literals, but inside a type position:

TS
// A simple string concatenation at type level
type Greeting = `Hello, ${string}!`;

const g1: Greeting = "Hello, Alice!";  // ✅
const g2: Greeting = "Hello, Bob!";    // ✅
// const g3: Greeting = "Hi, Carol!";  // ❌ doesn't match pattern

// Combine string literals
type Direction = "left" | "right" | "up" | "down";
type Prefix = "move" | "rotate";

type Action = `${Prefix}_${Direction}`;
// "move_left" | "move_right" | "move_up" | "move_down"
// | "rotate_left" | "rotate_right" | "rotate_up" | "rotate_down"
Note
When a template literal type contains a union, TypeScript automatically distributes over all combinations — producing a cross-product of every possible string.
String Interpolation with Unions

TypeScript expands template literal types over unions automatically. The result is the cartesian product of every combination — useful for generating exhaustive sets of string literals.

TS
type Size = "sm" | "md" | "lg";
type Colour = "red" | "green" | "blue";

type ButtonVariant = `btn-${Size}-${Colour}`;
// "btn-sm-red" | "btn-sm-green" | "btn-sm-blue"
// | "btn-md-red" | "btn-md-green" | "btn-md-blue"
// | "btn-lg-red" | "btn-lg-green" | "btn-lg-blue"
// (9 members total)

// TypeScript rejects any string outside this set
function applyStyle(variant: ButtonVariant, el: HTMLElement): void {
  el.classList.add(variant);
}

applyStyle("btn-md-red", document.body);      // ✅
// applyStyle("btn-xl-red", document.body);   // ❌ Type error
Intrinsic String Manipulation Types

TypeScript ships four built-in intrinsic types for string manipulation. They are implemented natively in the compiler (not expressible as regular TypeScript code):

  • Uppercase<S> — turns every character uppercase
  • Lowercase<S> — turns every character lowercase
  • Capitalize<S> — capitalises only the first character
  • Uncapitalize<S> — lowercases only the first character

TS
type U = Uppercase<"hello world">;    // "HELLO WORLD"
type L = Lowercase<"HELLO WORLD">;    // "hello world"
type C = Capitalize<"hello world">;   // "Hello world"
type N = Uncapitalize<"Hello World">; // "hello World"

// They work on unions too
type Events = "click" | "focus" | "blur";

type UpperEvents = Uppercase<Events>;     // "CLICK" | "FOCUS" | "BLUR"
type CapEvents   = Capitalize<Events>;    // "Click" | "Focus" | "Blur"
Tip
Capitalize is the most commonly used of the four — it lets you build camelCase getter/setter/handler names from lowercase property keys.
Building Event Name Patterns

A classic use case for template literal types is generating type-safe event handler names. You can derive the full set of valid handler names from an event map type:

TS
// Define event payloads
interface DOMEvents {
  click: MouseEvent;
  focus: FocusEvent;
  blur: FocusEvent;
  keydown: KeyboardEvent;
  input: InputEvent;
}

// Generate "on" + CapitalisedEventName for every event
type DOMEventHandlers = {
  [K in keyof DOMEvents as `on${Capitalize<K & string>}`]: (
    event: DOMEvents[K]
  ) => void;
};
// {
//   onClick:   (event: MouseEvent) => void;
//   onFocus:   (event: FocusEvent) => void;
//   onBlur:    (event: FocusEvent) => void;
//   onKeydown: (event: KeyboardEvent) => void;
//   onInput:   (event: InputEvent) => void;
// }

// Useful as a prop interface for a generic component
type ButtonProps = Partial<DOMEventHandlers> & {
  label: string;
  disabled?: boolean;
};
CSS Custom Property Types

Template literal types are a natural fit for CSS property names, since CSS follows strict naming conventions that can be captured as type-level patterns.

TS
// Match any CSS custom property (--var-name)
type CSSVar = `--${string}`;

const primary: CSSVar = "--color-primary";   // ✅
const spacing: CSSVar = "--spacing-4";       // ✅
// const invalid: CSSVar = "color-primary";  // ❌ must start with --

// Type-safe CSS custom property setter
function setCSSVar(element: HTMLElement, name: CSSVar, value: string): void {
  element.style.setProperty(name, value);
}

setCSSVar(document.body, "--font-size", "16px"); // ✅

// Typed theme token map
type ThemeToken = "color" | "spacing" | "radius" | "shadow";
type Scale = 1 | 2 | 3 | 4 | 5 | 6 | 8 | 10 | 12;

type DesignToken = `--${ThemeToken}-${Scale}`;
// "--color-1" | "--color-2" | ... | "--shadow-12"  (32 combinations)

type TokenMap = Partial<Record<DesignToken, string>>;
Combining with Mapped Types

Template literal types reach their full potential when combined with mapped types and key remapping. Together they let you derive entire interfaces from a source type.

TS
// Generate getter and setter pairs for every property
type AccessorPair<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
} & {
  [K in keyof T as `set${Capitalize<string & K>}`]: (value: T[K]) => void;
};

interface User {
  id: number;
  name: string;
  email: string;
}

type UserAccessors = AccessorPair<User>;
// {
//   getId:    () => number;
//   setId:    (value: number) => void;
//   getName:  () => string;
//   setName:  (value: string) => void;
//   getEmail: () => string;
//   setEmail: (value: string) => void;
// }

// Generate a "changed" event for every property
type ChangeEvents<T> = {
  [K in keyof T as `${string & K}Changed`]: (
    newValue: T[K],
    oldValue: T[K]
  ) => void;
};

type UserChangeEvents = ChangeEvents<User>;
// {
//   idChanged:    (newValue: number, oldValue: number) => void;
//   nameChanged:  (newValue: string, oldValue: string) => void;
//   emailChanged: (newValue: string, oldValue: string) => void;
// }
Extracting Parts of Template Literal Types with infer

Template literal types also work in conditional type extends clauses, where you can use infer to extract parts of a string type — like a compile-time regex capture group:

TS
// Extract the event name from an "on<Event>" handler name
type ExtractEventName<T extends string> =
  T extends `on${infer E}` ? Uncapitalize<E> : never;

type E1 = ExtractEventName<"onClick">;   // "click"
type E2 = ExtractEventName<"onFocus">;   // "focus"
type E3 = ExtractEventName<"onChange">;  // "change"
type E4 = ExtractEventName<"keydown">;   // never

// Extract the HTTP method and path from a route string
type ParseRoute<T extends string> =
  T extends `${infer Method} ${infer Path}`
    ? { method: Uppercase<Method>; path: Path }
    : never;

type R1 = ParseRoute<"get /users">;          // { method: "GET"; path: "/users" }
type R2 = ParseRoute<"post /users/:id">;     // { method: "POST"; path: "/users/:id" }
type R3 = ParseRoute<"invalid">;             // never
Tip
Combining template literal types with infer lets you parse string patterns at the type level — a powerful technique for typed routing, styled-system APIs, and more.
Practical API Pattern — Typed Router

Here is a real-world pattern: a type-safe HTTP router where the route definitions drive the entire type of your handler map:

TS
type HTTPMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";

// A route definition: "METHOD /path"
type RouteDef<M extends HTTPMethod, P extends string> = `${M} ${P}`;

// Extract path params (e.g. ":id", ":slug") from a path string
type ExtractParams<T extends string> =
  T extends `${string}:${infer Param}/${infer Rest}`
    ? Param | ExtractParams<`/${Rest}`>
    : T extends `${string}:${infer Param}`
      ? Param
      : never;

type Params1 = ExtractParams<"/users/:id">;          // "id"
type Params2 = ExtractParams<"/posts/:slug/comments/:commentId">; // "slug" | "commentId"
type Params3 = ExtractParams<"/about">;              // never

// Build a typed route handler
type RouteHandler<P extends string> = [ExtractParams<P>] extends [never]
  ? () => Response
  : (params: Record<ExtractParams<P>, string>) => Response;

declare const Response: unique symbol;
type Response = typeof Response;

type GetUserHandler = RouteHandler<"/users/:id">;
// (params: { id: string }) => Response

type GetHomeHandler = RouteHandler<"/home">;
// () => Response
Common Template Literal Type Patterns

Pattern

Example

Result

Prefix every key

on${Capitalize&lt;K&gt;}

onClick, onFocus

Suffix every key

${K}Changed

nameChanged, emailChanged

CSS variable

--${string}

--color-primary

Getter name

get${Capitalize&lt;K&gt;}

getId, getName

Setter name

set${Capitalize&lt;K&gt;}

setId, setName

HTTP route

${Method} ${Path}

GET /users

Extract suffix

T extends ${infer Base}Changed

nameChanged → name

Limitations to Be Aware Of
  • Template literal types only work with string, number, boolean, bigint, null, and undefined — not with object types

  • Very large cross-products (many unions × many unions) can slow down the TypeScript compiler significantly

  • You cannot use runtime string operations (slice, split, replace) inside template literal types — only the four intrinsic helpers are available

  • Circular template literal types will cause "Type alias circularly references itself" errors

Warning
Generating very large unions (thousands of members) with template literal types can noticeably slow down editor responsiveness and build times. Keep cross-products small — prefer narrower unions for the operands.
Real-World Example — Typed i18n Keys

Template literal types are excellent for i18n systems where translation keys follow a namespace.key naming convention:

TS
// Define your translation namespaces
interface Translations {
  auth: {
    login: string;
    logout: string;
    register: string;
  };
  dashboard: {
    title: string;
    welcome: string;
  };
  errors: {
    notFound: string;
    serverError: string;
  };
}

// Build dotted key paths: "auth.login" | "auth.logout" | ...
type DotKeys<T, P extends string = ""> = {
  [K in keyof T & string]:
    T[K] extends object
      ? DotKeys<T[K], `${P}${K}.`>
      : `${P}${K}`;
}[keyof T & string];

type I18nKey = DotKeys<Translations>;
// "auth.login" | "auth.logout" | "auth.register"
// | "dashboard.title" | "dashboard.welcome"
// | "errors.notFound" | "errors.serverError"

// Type-safe t() function
declare function t(key: I18nKey): string;

t("auth.login");         // ✅
t("dashboard.title");    // ✅
// t("auth.missing");    // ❌ Type error
Success
Template literal types let you encode string patterns, naming conventions, and structured identifiers directly in the type system — turning what would be runtime crashes into compile-time errors.