TypeScript 映射类型
最后修改:2025 年 3 月 5 日
TypeScript 中的映射类型允许您通过转换现有类型的属性来创建新类型。它们是强大的类型操作工具,能够实现灵活且可重用的类型定义。本教程将通过实际示例探讨映射类型。
映射类型使用 { [P in K]: T } 语法来迭代一组键 (K),并根据现有类型定义新的属性类型 (T)。结合 keyof 等功能,它们可以在保持类型安全的同时实现动态类型转换。
基本映射类型
基本映射类型将现有类型的所有属性转换为具有统一修改的新类型。
type User = {
name: string;
age: number;
};
type ReadonlyUser = {
readonly [K in keyof User]: User[K];
};
const user: ReadonlyUser = { name: "Alice", age: 25 };
// user.name = "Bob"; // Error: Cannot assign to 'name' because it is read-only
console.log(user.name); // Output: Alice
在此示例中,ReadonlyUser 是一个映射类型,它使用 keyof User 迭代 User 的键 (name 和 age),并为每个属性应用 readonly 修饰符,同时保留其原始类型 (User[K])。
结果类型确保 user 在初始化后无法被修改——尝试 user.name = "Bob" 会触发编译时错误。输出 "Alice" 证实了属性的值。这表明映射类型可以系统地转换属性,在此例中是强制不可变性,这对于保护数据完整性很有用。
可选属性
映射类型可以使现有类型的所有属性成为可选。
type Person = {
name: string;
age: number;
city: string;
};
type OptionalPerson = {
[P in keyof Person]?: Person[P];
};
const person: OptionalPerson = { name: "Bob" };
console.log(person); // Output: { name: "Bob" }
OptionalPerson 映射类型使用 ? 使 Person 的所有属性都成为可选。它迭代 keyof Person (name, age, city),并应用可选修饰符,同时保留原始类型 (Person[P])。这允许 person 在不发生错误的情况下省略 age 和 city,这与 Person 的必需结构不同。
输出 { name: "Bob" } 显示了这种灵活性。这种转换非常适合部分更新或表单等场景,其中并非所有字段都是必填的,展示了映射类型适应严格类型的能力。
类型转换
映射类型可以转换属性类型,例如将所有属性转换为不同的类型。
type Config = {
timeout: number;
retries: number;
};
type StringConfig = {
[K in keyof Config]: string;
};
const config: StringConfig = { timeout: "5000", retries: "3" };
console.log(config.timeout); // Output: 5000
在这里,StringConfig 将 Config 的所有属性从 number 类型转换为 string 类型。映射类型迭代 keyof Config (timeout, retries),并将每个属性的类型重新定义为 string,忽略原始的 Config[K] 类型。因此,config 对象接受字符串值,TypeScript 会强制执行此操作——timeout: 5000 会失败。
输出 "5000" 反映了转换后的类型。这说明了映射类型如何彻底改变属性类型,这对于调整数据格式(例如,基于字符串的配置)同时保持结构一致性非常有用。
过滤属性
映射类型可以使用条件类型根据条件过滤属性。
type Item = {
id: number;
name: string;
price: number;
};
type NumbersOnly<T> = {
[K in keyof T]: T[K] extends number ? T[K] : never;
};
const item: NumbersOnly<Item> = { id: 1, name: never, price: 100 };
// TypeScript errors if `name` is not `never`: Type 'string'
// is not assignable to type 'never'
console.log(item.price); // Output: 100
NumbersOnly 映射类型过滤 Item 的属性,只保留类型为 number 的属性。它在映射中使用条件类型 (T[K] extends number ? T[K] : never):对于 keyof Item 中的每个键,如果属性类型是 number(例如 id, price),则保留它;否则(例如 name: string),它将变为 never。
item 对象必须为 name 分配 never,从而在实践中有效地排除它,而 id 和 price 仍然可用。输出 100 证实了 price 的包含。这表明映射类型可以有选择地精炼类型,尽管 never 属性在实际使用中通常会被省略,以获得更简洁的对象。
带联合类型的映射类型
映射类型可以操作联合类型,为每个成员生成新类型。
type Keys = "name" | "age";
type Flags = {
[K in Keys]: boolean;
};
const flags: Flags = { name: true, age: false };
console.log(flags.name); // Output: true
Flags 映射类型迭代联合类型 Keys ("name" | "age"),创建一个将每个键映射到 boolean 的类型。与处理对象类型的 keyof 不同,这里键被明确定义为联合类型,生成 { name: boolean, age: boolean }。
flags 对象符合此结构,允许为每个键分配布尔值。输出 true 反映了 flags.name。此方法对于根据预定义的键集创建标志状对象或字典非常有用,它展示了映射类型在联合类型上的灵活性,超出了基于对象的键。
组合修饰符
映射类型可以组合多个修饰符,例如 readonly 和 ?。
type Product = {
name: string;
price: number;
};
type OptionalReadonlyProduct = {
readonly [K in keyof Product]?: Product[K];
};
const product: OptionalReadonlyProduct = { name: "Laptop" };
// product.name = "Tablet"; // Error: Cannot assign to 'name' because it is read-only
console.log(product.name); // Output: Laptop
OptionalReadonlyProduct 映射类型将 readonly 和 ? 应用于 Product 的属性。它迭代 keyof Product (name, price),使每个属性成为可选 (?) 且不可变 (readonly),同时保留原始类型 (Product[K])。
product 对象可以省略 price,并且在初始化后无法修改 name,如错误注释所示。输出 "Laptop" 证实了该值。这种组合对于定义灵活、不可变的数据结构非常有用,例如不应在设置后更改的可选配置对象。
结合工具类型进行映射
TypeScript 的内置工具类型通常使用映射类型进行常见的转换。
type User = {
name: string;
age: number;
};
const partialUser: Partial<User> = { name: "Alice" };
const pickedUser: Pick<User, "name"> = { name: "Bob" };
console.log(partialUser.name); // Output: Alice
console.log(pickedUser.name); // Output: Bob
此示例使用了 Partial 和 Pick,这两种工具类型都基于映射类型。Partial<user> 映射 keyof User (name, age),添加 ? 使属性可选,从而允许 partialUser 省略 age。
Pick<User, "name"> 只映射指定的键 ("name"),创建一个只包含该属性的类型,因此 pickedUser 只包含 name。输出 "Alice" 和 "Bob" 反映了这些转换后的类型。这些工具在内部利用映射类型,简化了部分对象或属性选择等常见模式,使开发人员无需编写自定义映射。
条件映射类型
映射类型可以包含条件逻辑来动态转换属性。
type Data = {
id: number;
name: string;
active: boolean;
};
type StringToNumber<T> = {
[K in keyof T]: T[K] extends string ? number : T[K];
};
const transformed: StringToNumber<Data> = { id: 1, name: 42, active: true };
console.log(transformed.name); // Output: 42
StringToNumber 映射类型有条件地转换 Data 属性:对于 keyof Data 中的每个键,如果属性类型扩展了 string(name),则它变为 number;否则,它保留原始类型(id: number, active: boolean)。transformed 对象反映了这一点——name 必须是数字 (42),而 id 和 active 保持不变。
输出 42 证实了转换。这种映射类型与条件的这种高级用法支持选择性类型更改,对于数据规范化或 API 响应映射非常有用。
带模板字面量键的映射类型
映射类型可以使用模板字面量类型动态生成新键。
type Event = "click" | "hover";
type EventHandlers = {
[K in `on${Capitalize<Event>}`]: () => void;
};
const handlers: EventHandlers = {
onClick: () => console.log("Clicked"),
onHover: () => console.log("Hovered")
};
handlers.onClick(); // Output: Clicked
EventHandlers 映射类型使用模板字面量将 Event 联合类型("click" | "hover")转换为 onClick 和 onHover 等键。Capitalize 工具大写每个事件,映射为每个键分配一个函数类型(() => void)。
handlers 对象符合此要求,为每个生成的键定义了方法。调用 onClick 会记录 "Clicked",如所示。此技术对于从简单的联合类型创建动态 API(例如事件系统)非常强大,它利用了映射类型能够创造性地操作键的能力。
最佳实践
- 使用映射类型进行转换: 应用映射类型系统地修改现有类型,增强可重用性而无需冗余定义。
- 利用 Keyof 保证安全: 将
keyof与映射类型配对,以确保所有属性都得到处理,从而保持类型一致性。 - 结合工具类型: 在编写自定义映射类型之前,使用
Partial或Pick等内置工具类型进行常见转换。 - 保持条件清晰: 在映射中使用条件类型时,请确保逻辑简单明了,以避免类型过于复杂或难以阅读。
- 记录复杂映射: 添加注释来解释复杂的映射类型(例如,使用模板字面量或条件类型),以供团队理解。
- 测试转换后的类型: 使用不同的输入来验证对象与映射类型的匹配情况,以确保转换符合预期。
- 避免过度使用: 将映射类型保留用于需要动态转换的场景,在修改量较少时选择更简单的类型。
来源
本教程通过实际示例介绍了 TypeScript 映射类型。使用这些技术来创建灵活、可重用且类型安全的代码转换。