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:
// 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"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.
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 errorIntrinsic 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 uppercaseLowercase<S>— turns every character lowercaseCapitalize<S>— capitalises only the first characterUncapitalize<S>— lowercases only the first character
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"
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:
// 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.
// 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.
// 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:
// 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">; // neverinfer 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:
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">;
// () => ResponseCommon Template Literal Type Patterns
Pattern | Example | Result |
|---|---|---|
Prefix every key |
| onClick, onFocus |
Suffix every key |
| nameChanged, emailChanged |
CSS variable |
| --color-primary |
Getter name |
| getId, getName |
Setter name |
| setId, setName |
HTTP route |
| GET /users |
Extract suffix | T extends | 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
Real-World Example — Typed i18n Keys
Template literal types are excellent for i18n systems where translation keys follow
a namespace.key naming convention:
// 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