TypeScript Mapped Types
Mapped Types are a powerful feature in TypeScript for creating new types based on existing types.
They allow developers to modify property characteristics in batch, such as making all properties optional or all properties readonly.
Mapped Types are the core technology for implementing TypeScript's built-in utility types.
SVG Diagram: How Mapped Types Work
Why Need Mapped Types
In practical TypeScript development, we often need to create variants based on existing types.
For example, a version where all properties are optional, or a version where all properties are readonly.
The traditional way is to define these types manually, which is tedious and error-prone.
Mapped types provide a declarative way to automatically generate these type variants.
Concept explanation:Mapped types use
keyofandinthe keyword to iterate over all keys of an existing type, then apply the same type transformation to each key.
Basic Mapped Types
Basic mapped types create new types by iterating over all keys of the original type.
This is the foundation for implementing utility types like Partial and Readonly.
Example
interface User {
// User ID
id: number;
// Username
name: string;
// User email
email: string;
}
// Implement the Partial utility type
// Iterate over all keys of T, add optional modifier ?
type Partial<T> = {
// P iterates over all keys returned by keyof T
// T[P] obtains the value type of the corresponding key in the original type
[P in keyof T]?: T[P];
};
// Use the Partial type
type PartialUser = Partial<User>;
// The PartialUser type is equivalent to:
// { id?: number; name?: string; email?: string }
// Only partial properties can be provided
var user: PartialUser = { name: "Alice" };
console.log("Partial user: " + JSON.stringify(user));
Output:
部分用户: {"name":"Alice"}
Syntax explanation:
[P in keyof T]Indicates iterating over all keys of type T.?The modifier makes properties optional.
Property Modifiers
Mapped types support multiple property modifiers to change property characteristics.
These modifiers can be combined to meet different type transformation needs.
Example
interface User {
// Username
name: string;
// User age
age: number;
}
// Use readonly mapping: make all properties readonly
// Add readonly modifier
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
// Use optional mapping: make all properties optional (basic version)
type Optional<T> = {
[P in keyof T]?: T[P];
};
// Use -? mapping: remove the optional modifier (make required)
// -? removes the original ? modifier
type Required<T> = {
[P in keyof T]-?: T[P];
};
// Test readonly type
var readonlyUser: Readonly<User> = { name: "Alice", age: 25 };
// readonlyUser.age = 30; // Error: readonly property cannot be modified
// Test optional type
var optionalUser: Optional<User> = { name: "Bob" };
console.log("Readonly: " + JSON.stringify(readonlyUser));
console.log("Optional: " + JSON.stringify(optionalUser));
Output:
只读: {"name":"Alice","age":25}
可选: {"name":"Bob"}
Modifier description:
?adds optional,-?removes optional;readonlyadds readonly,-readonlyremoves readonly.
Key Remapping
Mapped types can also useasthe keyword to remap key names.
This is useful when you need to uniformly modify key name formats.
Example
interface User {
// User ID
id: number;
// Username
name: string;
// User age
age: number;
}
// Use the as keyword to remap key names
// Add a prefix to all keys
type WithPrefix<T, Prefix extends string> = {
// Use template literal types to rename keys
// Capitalize makes the first letter uppercase
[P in keyof T as `${Prefix}${Capitalize<string & P>}`]: T[P];
};
// Use WithPrefix to add the "user" prefix
type PrefixedUser = WithPrefix<User, "user">;
// Converted type:
// { userId: number; userName: string; userAge: number }
// Use the type with prefix
var user: PrefixedUser = { userId: 1, userName: "Alice", userAge: 25 };
console.log("With prefix: " + JSON.stringify(user));
Output:
带前缀: {"userId":1,"userName":"Alice","userAge":25}
Template literal types:Use
`${Prefix}${Capitalizecan dynamically generate new key names.}`
Key Filtering
By combining conditional types and mapped types, key filtering can be achieved.
This is very useful when implementing utility types like Omit.
Example
interface User {
// User ID
id: number;
// Username
name: string;
// User password
password: string;
// User email
email: string;
}
// Implement Omit: exclude specified keys
// Use conditional types to filter keys
type Omit<T, K extends keyof T> = {
// P iterates over all keys of T
// If P is assignable to K (i.e., in the exclusion list), return never (excluded)
// Otherwise return P (keep the key)
[P in keyof T as P extends K ? never : P]: T[P];
};
// Use Omit to exclude the password key
type UserWithoutPassword = Omit<User, "password">;
// Converted type:
// { id: number; name: string; email: string }
// Use the type after excluding password
var user: UserWithoutPassword = { id: 1, name: "Alice", email: "[email protected]" };
console.log("Without password: " + JSON.stringify(user));
Output:
无密码: {"id":1,"name":"Alice","email":"[email protected]"}
never type:In mapped types, using never as the property type causes the property to be completely removed.
Conditional Mapping
Mapped types can be combined with conditional types to apply different transformations based on property types.
This makes type transformation more flexible and intelligent.
Example
interface APIResponse {
// Response data
data: string;
// Error message
error: string;
// Whether loading
isLoading: boolean;
// Timestamp
timestamp: number;
}
// Convert function types to () => void
// Iterate over all properties and perform conditional conversion based on property type
type FunctionToVoid<T> = {
// If T[P] is a function type, convert it to () => void
// Otherwise keep the original type unchanged
[P in keyof T]: T[P] extends (...args: any[]) => any
? () => void
: T[P];
};
// Use conditional mapping
var response: FunctionToVoid<APIResponse> = {
data: "hello",
error: "",
isLoading: false,
timestamp: Date.now()
};
console.log("Response: " + JSON.stringify(response));
Output:
响应: {"data":"hello","error":"","isLoading":false,"timestamp":...}
Application scenarios:Conditional mapping is often used in scenarios where different handling is needed based on types, such as processing API responses or cleaning up configuration objects.
Built-in Mapped Types
TypeScript has many built-in utility types implemented based on mapped types.
These utility types can satisfy most daily development needs.
Example
type P1 = Partial<{ a: string; b: number }>;
// Result: { a?: string; b?: number }
// Required - makes all optional properties required
type R1 = Required<{ a?: string; b?: number }>;
// Result: { a: string; b: number }
// Readonly - makes all properties readonly
type RO1 = Readonly<{ a: string; b: number }>;
// Result: { readonly a: string; readonly b: number }
// Pick - Select specified properties
type PK = Pick<{ a: string; b: number; c: boolean }, "a" | "b">;
// Result: { a: string; b: number }
// Omit - Exclude specified properties
type OM = Omit<{ a: string; b: number; c: boolean }, "c">;
// Result: { a: string; b: number }
// Test Partial
console.log("Partial: " + JSON.stringify({} as P1));
// Test Pick
console.log("Pick: " + JSON.stringify({ a: "x" } as PK));
Utility type combinations:These built-in utility types are all implemented based on mapped types and conditional types. Understanding how they work can help you use them better.
Notes
- keyof keyword:Used to obtain a union type composed of all keys of a type.
- in keyword:Used to iterate over union types of key names.
- Modifier position:
?andreadonlyBefore the property name, indicates adding a modifier. - Minus modifier:
-?and-readonlyUsed to remove modifiers. - as keyword:Used to remap key names; must return a string or numeric literal type.
Advanced:Mapped types can be combined with conditional types and template literal types to implement complex type transformations.
Summary
Mapped types are one of the most powerful features in the TypeScript type system.
- keyof:Get all keys of a type
- in:Iterate over key names for mapping
?:Add optional modifiers- readonly:Add readonly modifiers
- -?:Remove optional modifiers
- as:Remap key names
Best practices:Using mapped types wisely can greatly reduce repetitive type definitions and improve code maintainability.
Other extensions