⚡IntermediateAdvanced Type System
Template Literal Types (Advanced)
Build powerful string union types, remap object keys, and use built-in string manipulation types to enforce naming conventions at the type level.
Overview
Template literal types extend the backtick string syntax you know from JavaScript into the **type system**. At the beginner level you saw simple patterns like `/api/${string}`. At the intermediate level, combining template literals with **union distribution**, **key remapping** (`as` in mapped types), and the built-in **string manipulation types** (`Uppercase`, `Lowercase`, `Capitalize`, `Uncapitalize`) unlocks a whole class of patterns.
**Union distribution** means TypeScript automatically expands template literals over union members. The type `on${"click" | "focus"}` (written with backticks in real TS code) produces `"onClick" | "onFocus"` - TypeScript substitutes each union member and merges the results into a new string union.
**Key remapping** with `as` inside mapped types lets you rename keys while iterating: the pattern `[K in keyof T as `get${Capitalize<string & K>}`]` transforms any object type into a corresponding getter interface.
These patterns let you express naming conventions, event systems, and accessor patterns entirely in types - no runtime code needed.
Key Points
- •Template literal types use backtick syntax at the type level: `` type R = `/api/${string}` ``
- •Union distribution: TypeScript expands template literals over each union member automatically
- •Built-in string manipulation types: `Uppercase<S>`, `Lowercase<S>`, `Capitalize<S>`, `Uncapitalize<S>`
- •Key remapping: `[K in keyof T as NewKey]` - rename keys while mapping
- •`string & K` is needed when K is `string | number | symbol` but you need only the string keys
- •Template literals combine with `infer` to parse string shapes at the type level
- •Use `never` in key remapping to exclude specific keys from the resulting type
Examples
Union distribution and string manipulation types
type EventName = 'click' | 'focus' | 'blur' | 'change';
// Distribute over union - creates "onClick" | "onFocus" | "onBlur" | "onChange"
type HandlerName = `on${Capitalize<EventName>}`;
// Map back to handler function types
type EventHandlers = {
[E in EventName as `on${Capitalize<E>}`]: (event: Event) => void;
};
// Usage: EventHandlers is { onClick: ...; onFocus: ...; onBlur: ...; onChange: ... }
// HTTP method + resource names
type Method = 'get' | 'post' | 'put' | 'delete';
type Resource = 'User' | 'Post' | 'Comment';
type ApiMethod = `${Method}${Resource}`;
// "getUser" | "postUser" | ... 12 combinations generated automatically
function callApi(method: ApiMethod): void {
console.log(`Calling: ${method}`);
}
callApi('getUser'); // OK
callApi('deletePost'); // OK
// callApi('fetchUser'); // ErrorKey remapping - generating getter/setter interfaces
interface Person {
name: string;
age: number;
email: string;
}
// Generate getter methods for every property
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
// Generate setter methods for every property
type Setters<T> = {
[K in keyof T as `set${Capitalize<string & K>}`]: (value: T[K]) => void;
};
type PersonGetters = Getters<Person>;
// { getName: () => string; getAge: () => number; getEmail: () => string }
type PersonSetters = Setters<Person>;
// { setName: (value: string) => void; ... }
// Exclude specific keys with never
type OmitById<T> = {
[K in keyof T as K extends 'id' ? never : K]: T[K];
};
interface User { id: number; name: string; email: string }
type UserWithoutId = OmitById<User>; // { name: string; email: string }Parsing string patterns with infer
// Extract the resource from an API route string
type ExtractResource<T extends string> =
T extends `/api/${infer Resource}` ? Resource : never;
type A = ExtractResource<'/api/users'>; // "users"
type B = ExtractResource<'/api/products'>; // "products"
type C = ExtractResource<'/home'>; // never
// Parse "on{Event}" handler names back to event names
type ExtractEvent<T extends string> =
T extends `on${infer E}` ? Uncapitalize<E> : never;
type D = ExtractEvent<'onClick'>; // "click"
type E = ExtractEvent<'onFocus'>; // "focus"
// CSS property builder
type Side = 'top' | 'right' | 'bottom' | 'left';
type SpacingProp = 'margin' | 'padding';
type SpacingKey = `${SpacingProp}-${Side}` | SpacingProp;
// "margin" | "padding" | "margin-top" | "padding-left" | ...
type CSSSpacing = Partial<Record<SpacingKey, string | number>>;
const styles: CSSSpacing = {
margin: '0 auto',
'padding-top': 16,
'margin-left': '8px',
};Common Mistakes
- `K` in a mapped type is `string | number | symbol` - using it directly in a template literal errors; use `string & K` to narrow to only string keys
- Over-using template literal types for runtime validation - they only guard types at compile time, not values at runtime; use regex for that
- Forgetting that very large union distributions (hundreds of combinations) slow TypeScript down - keep combinatorial unions manageable
Note
The `as` clause in a mapped type is where key remapping happens: `[K in keyof T as NewKeyExpression]`. Combined with template literals, this is how you auto-generate APIs like `onPropertyChange` event maps or `getPropertyName` accessor interfaces from any existing type.
Exercises
Write TypeScript code to solve each exercise. Click Run to compile and execute it right here, then mark as done.
Loading editor…
Completed: 0 / 4 exercises
Knowledge Check
Question 1 of 4