Template Literal Types: String Operations at the Type Level
Basic Syntax
Template literal types use exactly the same syntax as JavaScript template strings — the only difference is that they operate on types instead of values.
type Greeting = `Hello, ${string}`;
const a: Greeting = "Hello, World"; // ✓
const b: Greeting = "Hello, Alice"; // ✓
const c: Greeting = "Hi, Alice"; // ✗ doesn't match the pattern
Concatenating concrete string literal types:
type Domain = "com" | "net" | "org";
type Protocol = "http" | "https";
type URL = `${Protocol}://${string}.${Domain}`;
const valid: URL = "https://example.com"; // ✓
const invalid: URL = "ftp://example.com"; // ✗ ftp is not in Protocol
Combining with Unions: Cartesian Products
When a template slot contains a union type, TypeScript automatically computes all combinations.
type Direction = "top" | "right" | "bottom" | "left";
type Axis = "X" | "Y";
type ScrollProperty = `scroll${Axis}`;
// "scrollX" | "scrollY"
type MarginProperty = `margin${Capitalize<Direction>}`;
// "marginTop" | "marginRight" | "marginBottom" | "marginLeft"
// Multiple union types — every combination
type Size = "sm" | "md" | "lg";
type Color = "primary" | "secondary";
type ButtonVariant = `${Color}-${Size}`;
// "primary-sm" | "primary-md" | "primary-lg"
// | "secondary-sm" | "secondary-md" | "secondary-lg"
Built-in String Manipulation Types
TypeScript ships four string manipulation utility types designed for template literals:
type S = "helloWorld";
type U = Uppercase<S>; // "HELLOWORLD"
type L = Lowercase<S>; // "helloworld"
type C = Capitalize<S>; // "HelloWorld"
type N = Uncapitalize<S>; // "helloWorld"
// Typical use: generate camelCase handler names
type EventName = "click" | "focus" | "blur";
type EventHandler = `on${Capitalize<EventName>}`;
// "onClick" | "onFocus" | "onBlur"
Parsing Strings with infer Inside Template Literals
Template literals combined with infer let you destructure string patterns.
// Extract everything after "on"
type ExtractAfterOn<T extends string> = T extends `on${infer Event}`
? Event
: never;
type A = ExtractAfterOn<"onClick">; // "Click"
type B = ExtractAfterOn<"onFocus">; // "Focus"
type C = ExtractAfterOn<"resize">; // never
// Extract head and tail of a dot-separated string
type Head<T extends string> = T extends `${infer H}.${string}` ? H : T;
type Tail<T extends string> = T extends `${string}.${infer R}` ? R : never;
type H = Head<"user.name">; // "user"
type R = Tail<"user.name">; // "name"
Real Use Case 1: Type-Safe CSS Property Names
type CSSProperty =
| "background-color"
| "border-radius"
| "font-size"
| "margin-top"
| "padding-left";
// Convert kebab-case to camelCase (recursive to handle multiple hyphens)
type KebabToCamel<T extends string> =
T extends `${infer Head}-${infer Tail}`
? `${Head}${Capitalize<KebabToCamel<Tail>>}`
: T;
type CamelCSSProperty = KebabToCamel<CSSProperty>;
// "backgroundColor" | "borderRadius" | "fontSize" | "marginTop" | "paddingLeft"
// Build a type-safe style object
type StyleObject = Partial<Record<CamelCSSProperty, string>>;
function applyStyle(element: HTMLElement, styles: StyleObject) {
for (const [key, value] of Object.entries(styles)) {
(element.style as Record<string, string>)[key] = value ?? "";
}
}
applyStyle(document.body, {
backgroundColor: "#fff", // ✓
borderRadius: "8px", // ✓
// backgroundColour: "#fff" // ✗ typo caught at compile time
});
Real Use Case 2: Type-Safe Event System Handler Names
interface DOMEvents {
click: MouseEvent;
focus: FocusEvent;
blur: FocusEvent;
keydown: KeyboardEvent;
resize: UIEvent;
}
// Generate on + capitalized handler names
type EventListeners = {
[K in keyof DOMEvents as `on${Capitalize<K>}`]: (event: DOMEvents[K]) => void;
};
// { onClick: (event: MouseEvent) => void; onFocus: (event: FocusEvent) => void; ... }
// Reverse: infer the event name from a handler name
type ExtractEvent<T extends string> = T extends `on${infer E}`
? Uncapitalize<E>
: never;
type E = ExtractEvent<"onClick">; // "click"
// Type-safe event registration
function addEventListener<K extends keyof DOMEvents>(
element: HTMLElement,
event: K,
handler: (e: DOMEvents[K]) => void
): void {
element.addEventListener(event, handler as EventListener);
}
addEventListener(document.body, "click", (e) => {
// e is inferred as MouseEvent — you get clientX, clientY, etc.
console.log(e.clientX, e.clientY);
});
Real Use Case 3: i18n Key Type Safety
// Define the shape of the translation file
interface Translations {
home: {
title: string;
subtitle: string;
cta: string;
};
about: {
title: string;
team: string;
};
errors: {
notFound: string;
serverError: string;
};
}
// Recursively generate all dot-notation paths
type DotPaths<T, Prefix extends string = ""> = {
[K in keyof T & string]: T[K] extends object
? DotPaths<T[K], `${Prefix}${K}.`>
: `${Prefix}${K}`;
}[keyof T & string];
type TranslationKey = DotPaths<Translations>;
// "home.title" | "home.subtitle" | "home.cta"
// | "about.title" | "about.team"
// | "errors.notFound" | "errors.serverError"
function t(key: TranslationKey): string {
return key; // implementation omitted — the key is guaranteed valid by type
}
t("home.title"); // ✓
t("about.team"); // ✓
// t("home.missing"); // ✗ key doesn't exist
// t("HOME.TITLE"); // ✗ case must match exactly
Real Use Case 4: Typed Express-Style Route Params
// Extract all :param names from a route string
type ExtractRouteParams<T extends string> =
T extends `${string}:${infer Param}/${infer Rest}`
? Param | ExtractRouteParams<`/${Rest}`>
: T extends `${string}:${infer Param}`
? Param
: never;
type Params1 = ExtractRouteParams<"/users/:id">;
// "id"
type Params2 = ExtractRouteParams<"/users/:userId/posts/:postId">;
// "userId" | "postId"
type Params3 = ExtractRouteParams<"/users/:userId/posts/:postId/comments/:commentId">;
// "userId" | "postId" | "commentId"
// Type-safe route handler
type RouteHandler<Path extends string> = (
params: Record<ExtractRouteParams<Path>, string>
) => void;
function defineRoute<Path extends string>(
path: Path,
handler: RouteHandler<Path>
) {
return { path, handler };
}
defineRoute("/users/:userId/posts/:postId", (params) => {
// params.userId ✓ — correctly inferred
// params.postId ✓
// params.postIdx ✗ — doesn't exist, compile error
console.log(params.userId, params.postId);
});
The Combinatorial Explosion Problem
Template literal types compute Cartesian products. Large unions can cause serious compiler slowdowns.
// Manageable: 6 × 10 = 60 combinations
type Weight = "100" | "200" | "300" | "400" | "500" | "600" | "700" | "800" | "900" | "950";
type Color = "red" | "green" | "blue" | "yellow" | "purple" | "gray";
type TailwindColor = `${Color}-${Weight}`; // 60 — acceptable
// Dangerous: 26 × 26 × 26 = 17,576 combinations
type Letter = "a" | "b" | "c" /* ... */ | "z";
type ThreeLetterCode = `${Letter}${Letter}${Letter}`; // do NOT do this
Rule of thumb: when the union exceeds ~1,000 members, switch to string with runtime validation.
// ✓ Use string + runtime validation instead
function setTailwindClass(className: string): void {
if (!isValidTailwindClass(className)) {
throw new Error(`Invalid class: ${className}`);
}
}
Anti-Patterns
// ❌ Anti-pattern: using template literals where a union would be clearer
type Endpoint = `${"GET" | "POST" | "PUT" | "DELETE"} ${string}`;
// string is too broad — you lose type safety on the path part
// ✓ Better: separate method and path types
type Method = "GET" | "POST" | "PUT" | "DELETE";
type KnownPath = "/users" | "/posts" | "/comments";
interface Request {
method: Method;
path: KnownPath;
}
// ❌ Anti-pattern: infer nested more than two levels — unreadable
type DeepExtract<T extends string> =
T extends `${infer A}_${infer B}_${infer C}_${infer D}`
? [A, B, C, D]
: T extends `${infer A}_${infer B}_${infer C}`
? [A, B, C]
: T extends `${infer A}_${infer B}`
? [A, B]
: [T];
// Works, but a runtime string.split("_") with typed output is far clearer
Comparison Table
| Use Case | Tool | Example |
|---|---|---|
| Concatenate string types | `${A}${B}` |
"get" + "User" → "getUser" |
| Capitalize first letter | Capitalize<S> |
"click" → "Click" |
| All uppercase | Uppercase<S> |
"get" → "GET" |
| Extract substring | infer in template |
"onClick" → "Click" |
| Expand unions | Insert union into slot | "a"|"b" → "xa"|"xb" |
| Route params | Recursive infer |
":id" → "id" |
| i18n paths | Recursive DotPaths |
Interface → all dot paths |
Chapter Summary
| Concept | Key Point |
|---|---|
| Basic syntax | Same as JS template strings; operates on types |
| Union expansion | Automatically produces Cartesian products |
| String utility types | Uppercase / Lowercase / Capitalize / Uncapitalize |
infer extraction |
Reverse-extract substrings from string patterns |
| Route parameters | Recursive infer extracts all :param tokens |
| i18n keys | Recursive DotPaths generates dot-path union |
| Combinatorial explosion | Over ~1,000 members: use string + runtime validation |
What's Next
Chapter 11 covers variance — covariance, contravariance, and invariance. You'll fully understand why (a: Animal) => void is assignable to (a: Dog) => void, and why Array<Dog> is not assignable to Array<Animal>. This is the foundation for understanding generic type safety.