索引类型

对于一个对象而言,我们可以使用属性名作为索引来访问属性值。相似地,对于一个对象类型而言,我们可以使用属性名作为索引来访问属性类型成员的类型。TypeScript 引入了两个新的类型结构来实现索引类型:

  • 索引类型查询。

  • 索引访问类型。

索引类型查询

通过索引类型查询能够获取给定类型中的属性名类型。索引类型查询的结果是由字符串字面量类型构成的联合类型,该联合类型中的每个字符串字面量类型都表示一个属性名类型。索引类型查询的语法如下所示:

keyof Type

在该语法中,keyof 是关键字,Type 表示任意一种类型。示例如下:

interface Point {
    x: number;
    y: number;
}

type T = keyof Point; // 'x' | 'y'

此例中,对 Point 类型使用索引类型查询的结果类型为联合类型 'x' | 'y',即由 Point 类型的属性名类型组成的联合类型。

索引类型查询解析

JavaScript 中的对象是键值对的数据结构,它只允许将字符串和 Symbol 值作为对象的键。索引类型查询获取的是对象的键的类型,因此索引类型查询的结果类型是联合类型 string | symbol 的子类型,因为只有这两种类型的值才能作为对象的键。但由于数组类型十分常用且其索引值的类型为 number 类型,因此编译器额外将 number 类型纳入了索引类型查询的结果类型范围。于是,索引类型查询的结果类型是联合类型 string | number | symbol 的子类型,这是编译器内置的类型约束。

例如,有如下索引类型查询:

type KeyofT = keyof T;

让我们来看看该索引类型查询的详细解析步骤。

如果类型 T 中包含字符串索引签名,那么将 string 类型和 number 类型添加到结果类型 KeyofT。示例如下:

interface T {
    [prop: string]: number;
}

// string | number
type KeyofT = keyof T;

如果类型 T 中包含数值索引签名,那么将 number 类型添加到结果类型 KeyofT。示例如下:

interface T {
    [prop: number]: number;
}

// number
type KeyofT = keyof T;

如果类型 T 中包含属性名类型为 unique symbol 的属性,那么将该 unique symbol 类型添加到结果类型 KeyofT。注意,如果想要在对象类型中声明属性名为 symbol 类型的属性,那么属性名的类型必须为 unique symbol 类型,而不允许为 symbol 类型。示例如下:

const s: unique symbol = Symbol();
interface T {
    [s]: boolean;
}

// typeof s
type KeyofT = keyof T;

因为 unique symbol 类型是 symbol 类型的子类型,所以该索引类型查询的结果类型仍是联合类型 string | number | symbol 的子类型。关于 unique symbol 类型的详细介绍请参考 5.3.5 节。

最后,如果类型 T 中包含其他属性成员,那么将表示属性名的字符串字面量类型和数字字面量类型添加到结果类型 Keyof T。例如,下例的类型 T 中包含三个属性成员,它们的属性名分别为数字 0、字符串 'a' 和字符串 'b'。因此,KeyofT 类型为联合类型 0 | 'a' | 'b'。示例如下:

interface T {
    0: boolean;
    a: string;
    b(): void;
}

// 0 | 'a' | 'b'
type KeyofT = keyof T;

以上我们介绍了在对象类型上使用索引类型查询的解析过程。虽然在对象类型上使用索引类型查询更有意义,但是索引类型查询也允许在非对象类型上使用,例如原始类型、顶端类型等。

当对 any 类型使用索引类型查询时,结果类型固定为联合类型 string | number | symbol。示例如下:

type KeyofT = keyof any;       // string | number | symbol

当对 unknown 类型使用索引类型查询时,结果类型固定为 never 类型。示例如下:

type KeyofT = keyof unknown;  // never

当对原始类型使用索引类型查询时,先查找与原始类型对应的内置对象类型,然后再进行索引类型查询。例如,与原始类型 boolean 对应的内置对象类型是 Boolean 对象类型,它的具体定义如下所示:

interface Boolean {
    valueOf(): boolean;
}

因此,对原始类型 boolean 执行索引类型查询的结果为字符串字面量类型 'valueOf'。示例如下:

type KeyofT = keyof boolean; // 'valueOf'

联合类型

在索引类型查询中,如果查询的类型为联合类型,那么先计算联合类型的结果类型,再执行索引类型查询。例如,有以下对象类型 AB,以及索引类型查询 KeyofT

type A = { a: string; z: boolean };
type B = { b: string; z: boolean };

type KeyofT = keyof (A | B);  // 'z'

在计算 KeyofT 类型时,先计算联合类型 A | B 的结果类型。示例如下:

type AB = A | B;         // { z: boolean }

然后计算索引类型查询 KeyofT 的类型。示例如下:

type KeyofT = keyof AB;  // 'z'

交叉类型

在索引类型查询中,如果查询的类型为交叉类型,那么会将原索引类型查询展开为子索引类型查询的联合类型,展开的规则类似于数学中的 乘法分配律。示例如下:

keyof (A & B) ≡ keyof A | keyof B

例如,有以下对象类型 AB,以及索引类型查询 KeyofT

type A = { a: string; x: boolean };
type B = { b: string; y: number };

type KeyofT = keyof (A & B); // 'a' | 'x' | 'b' | 'y'

在计算 KeyofT 类型时,先将索引类型查询展开为如下类型:

type KeyofT = keyof A | keyof B;

然后计算索引类型查询 KeyofT 的类型。示例如下:

type KeyofT = ('a' | 'x') | ('b' | 'y');

索引访问类型

索引访问类型能够获取对象类型中属性成员的类型,它的语法如下所示:

T[K]

在该语法中,TK 都表示类型,并且要求K类型必须能够赋值给 keyof T 类型。T[K] 的结果类型为 TK 属性的类型。

例如,有以下对象类型 T

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

通过索引访问类型能够获取对象类型 T 中属性 xy 的类型,示例如下:

type T = { x: boolean; y: string };

type Kx = 'x';
type T0 = T[Kx]; // boolean

type Ky = 'y';
type T1 = T[Ky]; // string

下面我们将深入介绍索引访问类型的详细解析步骤。假设有如下索引访问类型:

T[K]

K 是字符串字面量类型、数字字面量类型、枚举字面量类型或 unique symbol 类型,并且类型 T 中包含名为 K 的公共属性,那么 T[K] 的类型就是该属性的类型。示例如下:

const s: unique symbol = Symbol();

enum E {
    A = 10,
}

type T = {
    // 数字字面量属性名
    0: string;

    // 字符串字面量属性名
    x: boolean;

    // 枚举成员字面量属性名
    [E.A]: number;

    // unique symbol
    [s]: bigint;
};

type TypeOfNumberLikeName = T[0];     // string
type TypeOfStringLikeName = T['x'];   // boolean
type TypeOfEnumName = T[E.A];         // number
type TypeOfSymbolName = T[typeof s];  // bigint

K 是联合类型 K1 | K2,那么 T[K] 等于联合类型 T[K1] | T[K2]。例如,有以下类型 TK,其中 K 是联合类型:

type T = { x: boolean; y: string };
type K = 'x' | 'y';

那么,索引访问类型 T[K] 为如下类型:

// string | boolean
type TK = T['x'] | T['y'];

K 类型能够赋值给 string 类型,且类型 T 中包含字符串索引签名,那么 T[K] 为字符串索引签名的类型。但如果类型 T 中包含同名的属性,那么同名属性的类型拥有更高的优先级。示例如下:

interface T {
    a: true;
    [prop: string]: boolean;
}
type Ta = T['a'];          // true
type Tb = T['b'];          // boolean

K 类型能够赋值给 number 类型,且类型 T 中包含数值索引签名,那么 T[K] 为数值索引签名的类型。但如果类型 T 中包含同名的属性,那么同名属性的类型拥有更高的优先级。示例如下:

interface T {
    0: true;
    [prop: number]: boolean;
}

type T0 = T[0];          // true
type T1 = T[1];          // boolean

索引类型的应用

通过结合使用索引类型查询和索引访问类型就能够实现类型安全的对象属性访问操作。例如,下例中定义了工具函数 getProperty,它能够返回对象的某个属性值。该工具函数的特殊之处在于它还能够准确地返回对象属性的类型。示例如下:

function getProperty<T, K extends keyof T>(
  obj: T, key: K
): T[K] {
    return obj[key];
}

interface Circle {
    kind: 'circle';
    radius: number;
}

function f(circle: Circle) {
    // 正确,能够推断出 radius 的类型为 'circle' 类型
    const kind = getProperty(circle, 'kind');

    // 正确,能够推断出 radius 的类型为 number 类型
    const radius = getProperty(circle, 'radius');

    // 错误
    const unknown = getProperty(circle, 'unknown');
    //                                   ~~~~~~~~~
    // 编译错误:'unknown'类型不能赋值给'kind' |'radius'
}

第 14 和 17 行,我们使用 getProperty() 函数获取 Circle 对象中存在的属性,TypeScript 能够正确推断出获取的属性的类型。第 20 行,获取 Circle 对象中不存在的属性时,TypeScript 编译器能够检测出索引类型不匹配,因此产生编译错误。