映射对象类型

映射对象类型是一种独特的对象类型,它能够将已有的对象类型映射为新的对象类型。例如,我们想要将已有对象类型 T 中的所有属性修改为可选属性,那么我们可以直接修改对象类型 T 的类型声明,将每个属性都修改为可选属性。除此之外,更好的方法是使用映射对象类型将原对象类型 T 映射为一个新的对象类型 T,同时在映射过程中将每个属性修改为可选属性。

映射对象类型声明

映射对象类型是一个类型运算符,它能够遍历联合类型并以该联合类型的类型成员作为属性名类型来构造一个对象类型。映射对象类型声明的语法如下所示:

{ readonly [P in K]? : T }

在该语法中,readonly 是关键字,表示该属性是否为只读属性,该关键字是可选的;? 修饰符表示该属性是否为可选属性,该修饰符是可选的;in 是遍历语法的关键字;K 表示要遍历的类型,由于遍历的结果类型将作为对象属性名类型,因此类型 K 必须能够赋值给联合类型 string | number | symbol,因为只有这些类型的值才能作为对象的键;P 是类型变量,代表每次遍历出来的成员类型;T 是任意类型,表示对象属性的类型,并且在类型 T 中允许使用类型变量 P

映射对象类型的运算结果是一个对象类型。映射对象类型的核心是它能够遍历类型 K 的所有类型成员,并针对每一个成员 P 都将它映射为类型 T。示例如下:

type K = 'x' | 'y';
type T = number;

type MappedObjectType = { readonly [P in K]?: T };

此例中,映射对象类型 MappedObjectType 相当于如下对象类型:

{
    readonly x?: number;
    readonly y?: number;
}

映射对象类型解析

本节我们将深入映射对象类型的详细运算步骤。假设有如下映射对象类型:

{ [P in K]: T }

首先要强调的是,类型K必须能够赋值给联合类型 string | number | symbol

若当前遍历出来的类型成员 P 为字符串字面量类型,则在结果对象类型中创建一个新的属性成员,属性名类型为该字符串字面量类型且属性值类型为 T。示例如下:

// { x: boolean }
type MappedObjectType = { [P in 'x']: boolean };

若当前遍历出来的类型成员 P 为数字字面量类型,则在结果对象类型中创建一个新的属性成员,属性名类型为该数字字面量类型且属性值类型为 T。示例如下:

// { 0: boolean }
type MappedObjectType = { [P in 0]: boolean };

若当前遍历出来的类型成员 Punique symbol 类型,则在结果对象类型中创建一个新的属性成员,属性名类型为该 unique symbol 类型且属性值类型为 T。示例如下:

const s: unique symbol = Symbol();

// { [s]: boolean }
type MappedObjectType = { [P in typeof s]: boolean };

若当前遍历出来的类型成员 Pstring 类型,则在结果对象类型中创建字符串索引签名。示例如下:

// { [x: string]: boolean }
type MappedObjectType = { [P in string]: boolean };

若当前遍历出来的类型成员 Pnumber 类型,则在结果对象类型中创建数值索引签名。示例如下:

// { [x: number]: boolean }
type MappedObjectType = { [P in number]: boolean };

映射对象类型应用

将映射对象类型与索引类型查询结合使用就能够遍历已有对象类型的所有属性成员,并使用相同的属性来创建一个新的对象类型。示例如下:

type T = { a: string; b: number };

// { a: boolean; b: boolean;  }
type M = { [P in keyof T]: boolean };

此例中,映射对象类型能够遍历对象类型 T 的所有属性成员,并在新的对象类型 M 中创建同名的属性成员 ab,同时将每个属性成员的类型设置为 boolean 类型。我们使用了索引类型查询来获取类型 T 中所有属性名的类型并将其提供给映射对象类型进行遍历,两者能够完美地结合。

将映射对象类型、索引类型查询以及索引访问类型三者结合才能够最大限度地体现映射对象类型的威力。示例如下:

type T = { a: string; b: number };

// { a: string; b: number; }
type M = { [P in keyof T]: T[P] };

此例中,我们将对象类型 T 按原样复制了一份!在定义映射对象类型中的属性类型时,我们不再使用固定的类型,例如 boolean。借助于类型变量 P 和索引访问类型,我们能够动态地获取对象类型 T 中每个属性的类型。有了这个模板后,我们可以随意发挥,创建出一些有趣的类型。

例如,在本节开篇处我们提到将某个对象类型的所有属性成员修改为可选属性。借助于映射对象类型、索引类型查询以及索引访问类型可以很容易地创建出想要的对象类型。示例如下:

type T = { a: string; b: number };

// { a?: string; b?: number; }
type OptionalT = { [P in keyof T]?: T[P] };

我们仅在映射对象类型中添加了 ? 修饰符就实现了这个功能。由于这个功能十分常用,所以 TypeScript 内置了一个工具类型 Partial<T> 来实现这个功能,内置的 Partial<T> 工具类型的定义如下所示:

/**
 * 将T中的所有属性标记为可选属性
 */
type Partial<T> = {
    [P in keyof T]?: T[P];
};

该工具类型是利用了映射对象类型的泛型类型别名,它有一个类型参数 T。该工具类型将传入的对象类型的所有属性标记为可选属性。Partial<T> 工具类型的使用方式如下所示:

type T = { a: string; b: number };

// { a?: string; b?: number; }
type OptionalT = Partial<T>;

接下来,我们再创建一个对象类型,将已有对象类型中所有属性标记为只读属性。示例如下:

type T = { a: string; b: number };

// { readonly a: string; readonly b: number; }
type ReadonlyT = { readonly [P in keyof T]: T[P] };

此例中,我们使用 readonly 修饰符将所有属性标记为只读属性。由于这个功能十分常用,所以 TypeScript 内置了一个工具类型 Readonly<T> 来实现这个功能,内置的 Readonly<T> 工具类型的定义如下所示:

/**
 * 将T中的所有属性标记为只读属性
 */
type Readonly<T> = {
    readonly [P in keyof T]: T[P];
};

该工具类型是利用了映射对象类型的泛型类型别名,它有一个类型参数 T。该工具类型将传入的对象类型的所有属性标记为只读属性。Readonly<T> 工具类型的使用方式如下所示:

type T = { a: string; b: number };

// { readonly a: string; readonly b: number; }
type ReadonlyT = Readonly<T>;

关于工具类型的详细介绍请参考 6.8 节。

同态映射对象类型

不论是 Partial<T> 映射对象类型还是 Readonly<T> 映射对象类型,都是将源对象类型T中的属性一一对应地映射到新的对象类型中。映射后的对象类型结构与源对象类型T的结构完全一致,我们将这种映射对象类型称为同态映射对象类型。同态映射对象类型与源对象类型之间有着相同的属性集合。

如果映射对象类型中存在索引类型查询,那么 TypeScript 编译器会将该映射对象类型视为同态映射对象类型。更确切地说,同态映射对象类型具有如下语法形式:

{ readonly [P in keyof T]? : X }

在该语法中,readonly 关键字和 ? 修饰符均为可选的。示例如下:

type T = { a?: string; b: number };
type K = keyof T;

// 同态映射对象类型
type HMOT = { [P in keyof T]: T[P] };

// 非同态映射对象类型
type MOT = { [P in K]: T[P] };

修饰符拷贝

同态映射对象类型的一个重要性质是,新的对象类型会默认拷贝源对象类型中所有属性的 readonly 修饰符和 ? 修饰符。示例如下:

type T = { a?: string; readonly b: number };

// { a?: string; readonly b: number; }
type HMOT = { [P in keyof T]: T[P] };

此例中,HMOT 是同态映射对象类型,它将源对象类型 T 的所有属性映射到新的对象类型 HMOT,同时保留了每个属性的修饰符。例如,HMOT 对象类型的属性 a 带有 ? 修饰符,属性 b 带有 readonly 修饰符。

如果是非同态映射对象类型,那么新的对象类型不会拷贝源对象类型 T 中属性的 readonly 修饰符和 ? 修饰符。示例如下:

type T = { a?: string; readonly b: number };
type K = keyof T;

// { a: string | undefined; b: number; }
type MOT = { [P in K]: T[P] };

此例中,MOT 对象类型是映射对象类型但不是同态映射对象类型,因为它的语法中没有使用索引类型查询。非同态映射对象类型不会从源对象类型 T 中拷贝属性修饰符,因此在 MOT 对象类型中属性 ab 都没有修饰符。

改进的修饰符拷贝

为了改进映射对象类型中修饰符拷贝行为的一致性,TypeScript 特殊处理了映射对象类型中索引类型为类型参数的情况。假设有如下映射对象类型:

{ [P in K]: X }

如果在该语法中,K 为类型参数且有泛型约束 K extends keyof T,那么编译器也会将对象类型 T 的属性修饰符拷贝到映射对象类型中,尽管该类型不是同态映射对象类型。换句话说,当映射对象类型在操作已知对象类型的所有属性或部分属性时会拷贝属性修饰符到映射对象类型中。例如,有如下定义的映射对象类型 Pick

type Pick<T, K extends keyof T> = {
    [P in K]: T[P];
};

此例中,Pick 类型为非同态映射对象类型,因为它的语法中不包含索引类型查询。但是在 Pick 类型中,K 不是某一具体类型,而是一个类型参数,并且存在泛型约束 K extends keyof T。这时,TypeScript 会特殊处理这种形式的映射对象类型来保留属性修饰符。示例如下:

type T = {
    a?: string;
    readonly b: number;
    c: boolean;
};

// { a?: string; readonly b: number }
type SomeOfT = Pick<T, 'a' | 'b'>;

此例中,SomeOfT 对象类型中的属性 ab 保留了修饰符 ?readonly

此例中的 Pick 类型是十分常用的类型,它能够从已有对象类型中挑选一个或多个指定的属性并保留它们的类型和修饰符,然后构造出一个新的对象类型。因此,TypeScript 内置了该工具类型。关于工具类型的详细介绍请参考 6.8 节。

添加和移除修饰符

不论是同态映射对象类型的修饰符拷贝规则还是改进的映射对象类型修饰符拷贝规则,它们都无法删除属性已有的修饰符。因此,TypeScript 引入了两个新的修饰符用来精确控制添加或移除映射属性的 ? 修饰符和 readonly 修饰符:

  • + 修饰符,为映射属性添加 ? 修饰符或 readonly 修饰符。

  • – 修饰符,为映射属性移除 ? 修饰符或 readonly 修饰符。

+ 修饰符和 修饰符应用在 ? 修饰符和 readonly 修饰符之前。它们的语法如下所示:

{ -readonly [P in keyof T]-?: T[P] }

{ +readonly [P in keyof T]+?: T[P] }

如果要将已有对象类型的所有属性转换为必选属性,则可以使用 Required<T> 工具类型。Required<T> 类型的定义如下所示:

type Required<T> = { [P in keyof T]-?: T[P] };

Required<T> 类型创建了对象类型 T 的同态映射对象类型,并且移除了每个属性上的可选属性修饰符 ?。因此,同态映射对象类型中每一个映射属性都是必选属性。示例如下:

type T = {
    a?: string | undefined | null;
    readonly b: number | undefined | null;
};

// {
//     a: string | null;
//     readonly b: number | undefined | null;
// }
type RequiredT = Required<T>;

此例中,RequiredT 对象类型的每个属性成员都没有了 ? 修饰符。

需要注意的是, 修饰符仅作用于带有 ?readonly 修饰符的属性。编译器在移除属性 a? 修饰符时,同时会移除属性类型中的 undefined 类型,但是不会移除 null 类型,因此 RequiredT 类型中属性 a 的类型为 string | null 类型。由于属性 b 不带有 ? 修饰符,因此此例中的 修饰符对属性 b 不起作用,也不会移除属性 b 中的 undefined 类型。

Required<T> 类型是 TypeScript 内置的工具类型之一。关于工具类型的详细介绍请参考 6.8 节。

对于 + 修饰符,明确地添加它与省略它的作用是相同的,因此通常省略。例如,+readonly 等同于 readonly,如下所示:

type ReadonlyPartial<T> = {
    +readonly [P in keyof T]+?: T[P]
};

// 等同于:

type ReadonlyPartial<T> = {
    readonly [P in keyof T]?: T[P]
};

同态映射对象类型深入

同态映射对象类型是一种能够维持对象结构不变的映射对象类型。同态映射对象类型 { [P in keyof T]: X } 与对象类型 T 是同态关系,它们包含了完全相同的属性集合。在默认情况下,同态映射对象类型会保留对象类型 T 中属性的修饰符。

假设有如下同态映射对象类型,其中 TX 为类型参数:

type HMOT<T, X> = { [P in keyof T]: X };

现在来看看同态映射对象类型 HMOT<T, X> 的具体映射规则。

T 为原始类型,则不进行任何映射,同态映射对象类型 HMOT<T, X> 等于类型 T。示例如下:

type HMOT<T, X> = { [P in keyof T]: X };

type T = string;

type R = HMOT<T, boolean>  // <- 与boolean类型无关
       = string

T 为联合类型,则对联合类型的每个成员类型求同态映射对象类型,并使用每个结果类型构造一个联合类型。示例如下:

type HMOT<T, X> = { [P in keyof T]: X };

type T = { a: string } | { b: number };

type R = HMOT<T, boolean>;
       = HMOT<{ a: string }, boolean>
             | HMOT<{ b: number }, boolean>;
       = { a: boolean } | { b: boolean };

T 为数组类型,则同态映射对象类型 HMOT<T, X> 也为数组类型。示例如下:

type HMOT<T, X> = { [P in keyof T]: X };

type T = number[];

type R = HMOT<T, string>;
       = string[];

此时,若映射属性类型 X 为索引访问类型 T[P],则映射属性类型 X 等于数组 T 的成员类型。示例如下:

type HMOT<T> = { [P in keyof T]: T[P] };

type T = number[];

type R = HMOT<T>;
       = number[];

T 为只读数组类型,则同态映射对象类型 HMOT<T, X> 也为只读数组类型。示例如下:

type HMOT<T, X> = { [P in keyof T]: X };

type T = readonly number[];

type R = HMOT<T, string>;
       = readonly string[];

此时,若映射属性类型 X 为索引访问类型 T[P],则映射属性类型 X 等于数组 T 的成员类型。示例如下:

type HMOT<T> = { [P in keyof T]: T[P] };

type T = readonly number[];

type R = HMOT<T>;
       = readonly number[];

T 为元组类型,则同态映射对象类型 HMOT<T, X> 也为元组类型。示例如下:

type HMOT<T, X> = { [P in keyof T]: X };

type T = [string, number];

type R = HMOT<T, boolean>;
       = [boolean, boolean];

此时,若映射属性类型 X 为索引访问类型 T[P],则映射属性类型 X 等于元组 T 中对应成员的类型。示例如下:

type HMOT<T> = { [P in keyof T]: T[P] };

type T = [string, number];

type R = HMOT<T>;
       = [string, number];

T 为只读元组类型,则同态映射对象类型 HMOT<T, X> 也为只读元组类型。示例如下:

type HMOT<T, X> = { [P in keyof T]: X };

type T = readonly [string, number];

type R = HMOT<T, boolean>;
       = readonly [boolean, boolean];

此时,若映射属性类型 X 为索引访问类型 T[P],则映射属性类型 X 等于元组 T 中对应成员的类型。示例如下:

type HMOT<T> = { [P in keyof T]: T[P] };

type T = readonly [string, number];

type R = HMOT<T>;
       = readonly [string, number];

T 为数组类型或元组类型,且同态映射对象类型中使用了 readonly 修饰符,那么同态映射对象类型 HMOT<T, X> 的结果类型为只读数组类型或只读元组类型。示例如下:

type HMOT<T> = { readonly [P in keyof T]: T[P] };

type T0 = number[];

type R0 = HMOT<T0>;
        = readonly number[];

type T1 = [string];

type R1 = HMOT<T1>;
        = readonly [string];

T 为只读数组类型或只读元组类型,且同态映射对象类型中使用了 –readonly 修饰符,那么同态映射对象类型 HMOT<T, X> 的结果类型为非只读数组类型或非只读元组类型。示例如下:

type HMOT<T> = { -readonly [P in keyof T]: T[P] };

type T0 = readonly number[];

type R0 = HMOT<T0>;
        = number[];

type T1 = readonly [string];

type R1 = HMOT<T1>;
        = [string];