ZetCode

TypeScript 映射类型

最后修改:2025 年 3 月 5 日

TypeScript 中的映射类型允许您通过转换现有类型的属性来创建新类型。它们是强大的类型操作工具,能够实现灵活且可重用的类型定义。本教程将通过实际示例探讨映射类型。

映射类型使用 { [P in K]: T } 语法来迭代一组键 (K),并根据现有类型定义新的属性类型 (T)。结合 keyof 等功能,它们可以在保持类型安全的同时实现动态类型转换。

基本映射类型

基本映射类型将现有类型的所有属性转换为具有统一修改的新类型。

basic_mapped_type.ts
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 的键 (nameage),并为每个属性应用 readonly 修饰符,同时保留其原始类型 (User[K])。

结果类型确保 user 在初始化后无法被修改——尝试 user.name = "Bob" 会触发编译时错误。输出 "Alice" 证实了属性的值。这表明映射类型可以系统地转换属性,在此例中是强制不可变性,这对于保护数据完整性很有用。

可选属性

映射类型可以使现有类型的所有属性成为可选。

optional_properties.ts
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 在不发生错误的情况下省略 agecity,这与 Person 的必需结构不同。

输出 { name: "Bob" } 显示了这种灵活性。这种转换非常适合部分更新或表单等场景,其中并非所有字段都是必填的,展示了映射类型适应严格类型的能力。

类型转换

映射类型可以转换属性类型,例如将所有属性转换为不同的类型。

type_transformation.ts
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

在这里,StringConfigConfig 的所有属性从 number 类型转换为 string 类型。映射类型迭代 keyof Config (timeout, retries),并将每个属性的类型重新定义为 string,忽略原始的 Config[K] 类型。因此,config 对象接受字符串值,TypeScript 会强制执行此操作——timeout: 5000 会失败。

输出 "5000" 反映了转换后的类型。这说明了映射类型如何彻底改变属性类型,这对于调整数据格式(例如,基于字符串的配置)同时保持结构一致性非常有用。

过滤属性

映射类型可以使用条件类型根据条件过滤属性。

filter_properties.ts
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,从而在实践中有效地排除它,而 idprice 仍然可用。输出 100 证实了 price 的包含。这表明映射类型可以有选择地精炼类型,尽管 never 属性在实际使用中通常会被省略,以获得更简洁的对象。

带联合类型的映射类型

映射类型可以操作联合类型,为每个成员生成新类型。

union_mapped_type.ts
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?

combined_modifiers.ts
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 的内置工具类型通常使用映射类型进行常见的转换。

utility_mapped_type.ts
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

此示例使用了 PartialPick,这两种工具类型都基于映射类型。Partial<user> 映射 keyof User (name, age),添加 ? 使属性可选,从而允许 partialUser 省略 age

Pick<User, "name"> 只映射指定的键 ("name"),创建一个只包含该属性的类型,因此 pickedUser 只包含 name。输出 "Alice" 和 "Bob" 反映了这些转换后的类型。这些工具在内部利用映射类型,简化了部分对象或属性选择等常见模式,使开发人员无需编写自定义映射。

条件映射类型

映射类型可以包含条件逻辑来动态转换属性。

conditional_mapped_type.ts
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 中的每个键,如果属性类型扩展了 stringname),则它变为 number;否则,它保留原始类型(id: number, active: boolean)。transformed 对象反映了这一点——name 必须是数字 (42),而 idactive 保持不变。

输出 42 证实了转换。这种映射类型与条件的这种高级用法支持选择性类型更改,对于数据规范化或 API 响应映射非常有用。

带模板字面量键的映射类型

映射类型可以使用模板字面量类型动态生成新键。

template_literal_mapped_type.ts
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")转换为 onClickonHover 等键。Capitalize 工具大写每个事件,映射为每个键分配一个函数类型(() => void)。

handlers 对象符合此要求,为每个生成的键定义了方法。调用 onClick 会记录 "Clicked",如所示。此技术对于从简单的联合类型创建动态 API(例如事件系统)非常强大,它利用了映射类型能够创造性地操作键的能力。

最佳实践

来源

TypeScript 映射类型文档

本教程通过实际示例介绍了 TypeScript 映射类型。使用这些技术来创建灵活、可重用且类型安全的代码转换。

作者

我叫 Jan Bodnar,是一名充满热情的程序员,拥有丰富的编程经验。我自 2007 年以来一直在撰写编程文章。迄今为止,我已撰写了 1400 多篇文章和 8 本电子书。我在编程教学方面拥有十多年的经验。

列出所有 TypeScript 教程