当前位置:  首页>> 技术小册>> TypeScript开发实战

第20章 高级类型(3):映射类型

在TypeScript的世界里,高级类型不仅仅是语言的点缀,它们是构建复杂、灵活且安全类型系统的基石。本章将深入探讨TypeScript中的一个极为强大的特性——映射类型(Mapped Types),这一特性允许我们根据已存在的类型自动地创建新类型,极大地提高了类型定义的复用性和表达能力。

20.1 映射类型基础

映射类型是一种通过旧类型(我们称之为“源类型”)创建新类型的方式,新类型的每个属性都基于旧类型中相应属性的某种变换规则来定义。这种变换规则可以是修改属性的类型、添加或删除属性等。映射类型的基本语法如下:

  1. type MappedType<T> = {
  2. [P in keyof T]: /* 转换逻辑 */
  3. };

在这里,keyof T 获取了类型 T 的所有键的联合类型,而 [P in keyof T]: ... 则是对这些键进行遍历,并对每个键 P 应用转换逻辑来定义新类型的相应属性。

20.2 示例:只读与可选属性

20.2.1 只读映射类型

假设我们有一个基础类型,我们希望基于这个类型创建一个所有属性都是只读的版本。这可以通过映射类型轻松实现:

  1. type Readonly<T> = {
  2. readonly [P in keyof T]: T[P];
  3. };
  4. interface Todo {
  5. title: string;
  6. completed: boolean;
  7. }
  8. const todo: Readonly<Todo> = {
  9. title: "Learn TypeScript",
  10. completed: false,
  11. };
  12. // 错误:不能给 readonly 属性赋值
  13. todo.title = "Another Task"; // TypeScript 会报错
20.2.2 可选属性映射类型

类似地,如果我们想要一个所有属性都是可选的类型,也可以这样做:

  1. type Partial<T> = {
  2. [P in keyof T]?: T[P];
  3. };
  4. const partialTodo: Partial<Todo> = {
  5. completed: true, // title 是可选的,可以省略
  6. };

20.3 深入转换逻辑

映射类型的真正威力在于其转换逻辑的灵活性。我们可以对属性的类型进行复杂的变换,比如添加前缀、后缀,或者基于条件改变类型等。

20.3.1 类型变换示例

假设我们有一个用户信息类型,我们希望创建一个新类型,其所有属性类型都变为字符串:

  1. type Stringified<T> = {
  2. [P in keyof T]: string;
  3. };
  4. interface User {
  5. name: string;
  6. age: number;
  7. isAdmin: boolean;
  8. }
  9. type StringifiedUser = Stringified<User>;
  10. // 相当于:
  11. // type StringifiedUser = {
  12. // name: string;
  13. // age: string;
  14. // isAdmin: string;
  15. // };
20.3.2 条件类型在映射类型中的应用

条件类型(Conditional Types)与映射类型结合使用,可以实现更复杂的类型变换逻辑。例如,我们可能想要创建一个类型,其中只包含源类型中的数字类型属性:

  1. type NumbersOnly<T> = {
  2. [P in keyof T as T[P] extends number ? P : never]: T[P];
  3. };
  4. interface Mixed {
  5. id: number;
  6. name: string;
  7. age: number;
  8. isActive: boolean;
  9. }
  10. type NumbersFromMixed = NumbersOnly<Mixed>;
  11. // 相当于:
  12. // type NumbersFromMixed = {
  13. // id: number;
  14. // age: number;
  15. // };
  16. // 注意:isActive 和 name 不包含在内,因为它们不是数字类型

在这个例子中,as T[P] extends number ? P : never 是一个条件表达式,用于筛选键。如果属性类型是 number,则保留该键;否则,使用 never 排除它。

20.4 映射类型的高级用法

20.4.1 递归映射类型

有时,我们需要对嵌套的对象结构进行类型变换,这就需要用到递归映射类型。递归映射类型通常与泛型约束和条件类型结合使用,以处理复杂的嵌套结构。

  1. type DeepReadonly<T> = {
  2. readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P];
  3. };
  4. interface NestedObject {
  5. id: number;
  6. details: {
  7. name: string;
  8. attributes: {
  9. size: number;
  10. };
  11. };
  12. }
  13. const nested: DeepReadonly<NestedObject> = {
  14. id: 1,
  15. details: {
  16. name: "Example",
  17. attributes: {
  18. size: 100,
  19. },
  20. },
  21. };
  22. // 错误:不能修改深层嵌套属性
  23. nested.details.attributes.size = 200; // TypeScript 会报错
20.4.2 映射类型的键重命名

虽然TypeScript没有直接提供键重命名的映射类型语法,但我们可以通过结合索引签名和条件类型来模拟这一行为。这种方法相对复杂,通常涉及到映射类型内部的额外逻辑来捕获旧键并映射到新键。

20.5 实战应用

映射类型在实际项目中有着广泛的应用。例如,在构建REST API客户端库时,我们可能需要根据API响应自动生成TypeScript类型。通过映射类型,我们可以轻松地根据API的JSON结构生成TypeScript接口,从而简化类型定义工作,减少出错的可能性。

另外,在构建复杂的状态管理库(如Redux、Vuex)时,映射类型也非常有用。我们可以利用映射类型来自动推导状态树中每个部分的类型,或者为状态树的各个部分添加额外的属性(如加载状态、错误信息)而不必手动编写大量重复的代码。

20.6 小结

映射类型是TypeScript中一个非常强大的特性,它允许我们根据已存在的类型自动地创建新类型,极大地提高了类型定义的复用性和表达能力。通过掌握映射类型的基本语法和高级用法,我们可以编写出更加灵活、安全且易于维护的TypeScript代码。无论是在构建大型应用程序还是开发库和框架时,映射类型都是不可或缺的工具之一。


该分类下的相关小册推荐: