现在位置: 首页 > TypeScript 教程 > 正文

TypeScript 映射类型

映射类型(Mapped Types)是 TypeScript 中一种基于已有类型创建新类型的强大特性。

它允许开发者批量修改属性的特性,如将所有属性变为可选、将所有属性变为只读等。

映射类型是实现 TypeScript 内置工具类型的核心技术。


映射类型工作原理 原始类型 interface User { id: number; name: string; } 映射语法 { [P in keyof T]?: T[P] // 添加可选修饰符 } 结果类型 type PartialUser = { id?: number; name?: string; } 映射类型修饰符 ? 前缀 添加可选修饰符 type Partial<T> readonly 前缀 添加只读修饰符 type Readonly<T> -? 前缀 移除可选修饰符 type Required<T> as 关键字 重映射键名 as `${P}`

为什么需要映射类型

在实际的 TypeScript 开发中,我们经常需要基于现有类型创建变体。

例如,需要一个所有属性都是可选的版本,或者所有属性都是只读的版本。

传统的方式是手动定义这些类型,既繁琐又容易出错。

映射类型提供了一种声明式的方式来自动生成这些类型变体。

概念说明:映射类型使用 keyofin 关键字遍历已有类型的所有键,然后对每个键应用相同的类型转换。


基础映射类型

基础映射类型通过遍历原始类型的所有键来创建新类型。

这是实现 Partial、Readonly 等工具类型的基础。

实例

// 定义用户接口
interface User {
    // 用户 ID
    id: number;
    // 用户名
    name: string;
    // 用户邮箱
    email: string;
}

// 实现 Partial 工具类型
// 遍历 T 的所有键,添加可选修饰符 ?
type Partial<T> = {
    // P 遍历 keyof T 返回的所有键
    // T[P] 获取原类型中对应键的值类型
    [P in keyof T]?: T[P];
};

// 使用 Partial 类型
type PartialUser = Partial<User>;

// PartialUser 类型等同于:
// { id?: number; name?: string; email?: string }

// 可以只提供部分属性
var user: PartialUser = { name: "Alice" };

console.log("部分用户: " + JSON.stringify(user));

运行结果:

部分用户: {"name":"Alice"}

语法解释:[P in keyof T] 表示遍历 T 类型的所有键。? 修饰符将属性设为可选。


属性修饰符

映射类型支持多种属性修饰符来修改属性的特性。

这些修饰符可以组合使用,实现不同的类型转换需求。

实例

// 定义用户接口
interface User {
    // 用户名
    name: string;
    // 用户年龄
    age: number;
}

// 使用 readonly 映射:将所有属性变为只读
// 添加 readonly 修饰符
type Readonly<T> = {
    readonly [P in keyof T]: T[P];
};

// 使用可选映射:将所有属性变为可选(基础版)
type Optional<T> = {
    [P in keyof T]?: T[P];
};

// 使用 -? 映射:移除可选修饰符(变为必填)
// -? 会移除原有的 ? 修饰符
type Required<T> = {
    [P in keyof T]-?: T[P];
};

// 测试只读类型
var readonlyUser: Readonly<User> = { name: "Alice", age: 25 };
// readonlyUser.age = 30; // 错误:只读属性不能修改

// 测试可选类型
var optionalUser: Optional<User> = { name: "Bob" };

console.log("只读: " + JSON.stringify(readonlyUser));
console.log("可选: " + JSON.stringify(optionalUser));

运行结果:

只读: {"name":"Alice","age":25}
可选: {"name":"Bob"}

修饰符说明:? 添加可选,-? 移除可选;readonly 添加只读,-readonly 移除只读。


键名映射

映射类型还可以使用 as 关键字来重映射键名。

这在需要统一修改键名格式时非常有用。

实例

// 定义用户接口
interface User {
    // 用户 ID
    id: number;
    // 用户名
    name: string;
    // 用户年龄
    age: number;
}

// 使用 as 关键字重映射键名
// 为所有键添加前缀
type WithPrefix<T, Prefix extends string> = {
    // 使用模板字面量类型重命名键
    // Capitalize 将首字母大写
    [P in keyof T as `${Prefix}${Capitalize<string & P>}`]: T[P];
};

// 使用 WithPrefix 添加 "user" 前缀
type PrefixedUser = WithPrefix<User, "user">;

// 转换后的类型:
// { userId: number; userName: string; userAge: number }

// 使用带前缀的类型
var user: PrefixedUser = { userId: 1, userName: "Alice", userAge: 25 };

console.log("带前缀: " + JSON.stringify(user));

运行结果:

带前缀: {"userId":1,"userName":"Alice","userAge":25}

模板字面量类型:使用 `${Prefix}${Capitalize}` 可以动态生成新的键名。


键过滤

通过条件类型和映射类型的组合,可以实现键的过滤。

这在实现 Omit 等工具类型时非常有用。

实例

// 定义用户接口
interface User {
    // 用户 ID
    id: number;
    // 用户名
    name: string;
    // 用户密码
    password: string;
    // 用户邮箱
    email: string;
}

// 实现 Omit:排除指定键
// 使用条件类型过滤键
type Omit<T, K extends keyof T> = {
    // P 遍历 T 的所有键
    // 如果 P 可以赋值给 K(即在排除列表中),返回 never(不包含)
    // 否则返回 P(保留该键)
    [P in keyof T as P extends K ? never : P]: T[P];
};

// 使用 Omit 排除 password 键
type UserWithoutPassword = Omit<User, "password">;

// 转换后的类型:
// { id: number; name: string; email: string }

// 使用排除 password 后的类型
var user: UserWithoutPassword = { id: 1, name: "Alice", email: "a@b.com" };

console.log("无密码: " + JSON.stringify(user));

运行结果:

无密码: {"id":1,"name":"Alice","email":"a@b.com"}

never 类型:在映射类型中使用 never 作为属性类型,该属性会被完全移除。


条件映射

映射类型可以与条件类型结合,根据属性类型的不同应用不同的转换。

这使得类型转换更加灵活和智能。

实例

// 定义 API 响应接口
interface APIResponse {
    // 响应数据
    data: string;
    // 错误信息
    error: string;
    // 是否加载中
    isLoading: boolean;
    // 时间戳
    timestamp: number;
}

// 将函数类型转换为 () => void
// 遍历所有属性,根据属性类型进行条件转换
type FunctionToVoid<T> = {
    // 如果 T[P] 是函数类型,转换为 () => void
    // 否则保持原类型不变
    [P in keyof T]: T[P] extends (...args: any[]) => any
        ? () => void
        : T[P];
};

// 使用条件映射
var response: FunctionToVoid<APIResponse> = {
    data: "hello",
    error: "",
    isLoading: false,
    timestamp: Date.now()
};

console.log("响应: " + JSON.stringify(response));

运行结果:

响应: {"data":"hello","error":"","isLoading":false,"timestamp":...}

应用场景:条件映射常用于处理 API 响应、清理配置对象等需要根据类型做不同处理的场景。


内置映射类型

TypeScript 内置了许多基于映射类型实现的工具类型。

这些工具类型可以满足大多数日常开发需求。

实例

// Partial - 将所有属性变为可选
type P1 = Partial<{ a: string; b: number }>;
// 结果:{ a?: string; b?: number }

// Required - 将所有可选属性变为必填
type R1 = Required<{ a?: string; b?: number }>;
// 结果:{ a: string; b: number }

// Readonly - 将所有属性变为只读
type RO1 = Readonly<{ a: string; b: number }>;
// 结果:{ readonly a: string; readonly b: number }

// Pick - 选择指定的属性
type PK = Pick<{ a: string; b: number; c: boolean }, "a" | "b">;
// 结果:{ a: string; b: number }

// Omit - 排除指定的属性
type OM = Omit<{ a: string; b: number; c: boolean }, "c">;
// 结果:{ a: string; b: number }

// 测试 Partial
console.log("Partial: " + JSON.stringify({} as P1));

// 测试 Pick
console.log("Pick: " + JSON.stringify({ a: "x" } as PK));

工具类型组合:这些内置工具类型都是基于映射类型和条件类型实现的。了解其原理可以更好地使用它们。


注意事项

  • keyof 关键字:用于获取类型的所有键组成的联合类型
  • in 关键字:用于遍历键名联合类型
  • 修饰符位置:?readonly 在属性名前,表示添加修饰符
  • 减号修饰符:-?-readonly 用于移除修饰符
  • as 关键字:用于重映射键名,必须返回字符串或数字字面量类型

进阶:映射类型可以与条件类型、模板字面量类型组合,实现复杂的类型转换。


总结

映射类型是 TypeScript 类型系统中最强大的特性之一。

  • keyof:获取类型的所有键
  • in:遍历键名进行映射
  • ?添加可选修饰符
  • readonly:添加只读修饰符
  • -?:移除可选修饰符
  • as:重映射键名

最佳实践:善用映射类型可以大幅减少重复的类型定义,提高代码的可维护性。