Mapped types are one of TypeScript's most powerful features for creating new types by transforming existing ones. They allow you to iterate over the properties of a type and apply transformations to create a new type structure.
The basic syntax of a mapped type is { [P in K]: T }, where P is a property name variable, K is a union of keys to iterate over (often keyof SomeType), and T is the type for each property. Think of it like a for...in loop, but at the type level.
Mapped types become especially powerful when combined with key remapping and modifiers. You can add or remove modifiers like optional (?) and readonly using + and - prefixes. For example, [P in keyof T]?: T[P] makes all properties optional by adding the ? modifier, while [P in keyof T]-?: T[P] removes the optional modifier to make properties required.
Key filtering is another crucial technique. You can use conditional types like Exclude<keyof T, K> to filter out unwanted keys, or constrain your mapped type to only iterate over specific keys. This is the foundation for utility types like Pick and Omit.
The beauty of mapped types lies in their ability to preserve the relationship between property names and their types while applying systematic transformations. This makes them perfect for creating utility types that work generically across any object structure.
1// Example: Creating a type that makes specific properties optional
2type MakeOptional<T, K extends keyof T> = {
3 [P in keyof T]: P extends K ? T[P] | undefined : T[P];
4};
5
6// Usage example
7interface Person {
8 id: number;
9 name: string;
10 email: string;
11}
12
13// Makes 'email' optional while keeping others required
14type PersonWithOptionalEmail = MakeOptional<Person, 'email'>;
15// Result: { id: number; name: string; email: string | undefined; }