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 映射类型。使用这些技术来创建灵活、可重用且类型安全的代码转换。