TypeScript——索引类型、映射对象类型、条件类型

索引类型、映射对象类型、条件类型

1、索引类型

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

  • 索引类型查询。
  • 索引访问类型。

1.1、索引类型查询

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

ts 复制代码
keyof Type

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

ts 复制代码
interface Point {
    x: number;
    y: number;
}

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

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

1.1.1、索引类型查询解析

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

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

ts 复制代码
01 type KeyofT = keyof T;

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

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

ts 复制代码
interface T {
    [prop: string]: number;
}

// string | number
type KeyofT = keyof T;

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

ts 复制代码
const s: unique symbol = Symbol();
interface T {
    [s]: boolean;
}

// typeof s
type KeyofT = keyof T;

因为"unique symbol"类型是symbol类型的子类型,所以该索引类型查询的结果类型仍是联合类型"string | number | symbol"的子类型。

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

ts 复制代码
interface T {
    0: boolean;
    a: string;
    b(): void;
}

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

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

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

ts 复制代码
type KeyofT = keyof any;       // string | number | symbol

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

ts 复制代码
type KeyofT = keyof unknown;  // never

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

ts 复制代码
interface Boolean {
    valueOf(): boolean;
}

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

ts 复制代码
type KeyofT = keyof boolean; // 'valueOf'
1.1.2、联合类型

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

ts 复制代码
type A = { a: string; z: boolean };
type B = { b: string; z: boolean };

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

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

ts 复制代码
type AB = A | B;         // { z: boolean }

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

ts 复制代码
type KeyofT = keyof AB;  // 'z'
1.1.3、交叉类型

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

ts 复制代码
keyof (A & B) ≡ keyof A | keyof B

例如,有以下对象类型A和B,以及索引类型查询KeyofT:

ts 复制代码
type A = { a: string; x: boolean };
type B = { b: string; y: number };

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

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

ts 复制代码
type KeyofT = keyof A | keyof B;

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

ts 复制代码
type KeyofT = ('a' | 'x') | ('b' | 'y');

1.2、索引访问类型

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

ts 复制代码
T[K]

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

例如,有以下对象类型T:

ts 复制代码
type T = { a: boolean; b: string };

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

ts 复制代码
type T = { x: boolean; y: string };

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

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

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

ts 复制代码
T[K]

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

ts 复制代码
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]​"​。例如,有以下类型T和K,其中K是联合类型:

ts 复制代码
type T = { x: boolean; y: string };
type K = 'x' | 'y';

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

ts 复制代码
// string | boolean
type TK = T['x'] | T['y'];

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

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

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

ts 复制代码
interface T {
    0: true;
    [prop: number]: boolean;
}

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

1.3、索引类型的应用

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

ts 复制代码
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编译器能够检测出索引类型不匹配,因此产生编译错误。

2、映射对象类型

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

2.1、映射对象类型声明

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

ts 复制代码
{ readonly [P in K]? : T }

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

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

ts 复制代码
type K = 'x' | 'y';
type T = number;

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

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

ts 复制代码
{
    readonly x?: number;
    readonly y?: number;
}

2.2、映射对象类型解析

假设有如下映射对象类型:

ts 复制代码
{ [P in K]: T }

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

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

ts 复制代码
// { x: boolean }
type MappedObjectType = { [P in 'x']: boolean };

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

ts 复制代码
// { 0: boolean }
type MappedObjectType = { [P in 0]: boolean };

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

ts 复制代码
const s: unique symbol = Symbol();

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

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

ts 复制代码
// { [x: string]: boolean }
type MappedObjectType = { [P in string]: boolean };

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

ts 复制代码
// { [x: number]: boolean }
type MappedObjectType = { [P in number]: boolean };

2.3、映射对象类型应用

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

ts 复制代码
type T = { a: string; b: number };

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

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

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

ts 复制代码
type T = { a: string; b: number };

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

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

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

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

ts 复制代码
type T = { a: string; b: number };

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

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

ts 复制代码
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>"工具类型的定义如下所示:

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

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

ts 复制代码
type T = { a: string; b: number };

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

2.4、同态映射对象类型

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

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

ts 复制代码
{ readonly [P in keyof T]? : X }

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

ts 复制代码
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] };
2.4.1、修饰符拷贝

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

ts 复制代码
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修饰符和"?"修饰符。示例如下:

ts 复制代码
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对象类型中属性a和b都没有修饰符。

2.4.2、改进的修饰符拷贝

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

ts 复制代码
{ [P in K]: X }

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

ts 复制代码
type Pick<T, K extends keyof T> = {
    [P in K]: T[P];
};

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

ts 复制代码
type T = {
    a?: string;
    readonly b: number;
    c: boolean;
};

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

此例中,SomeOfT对象类型中的属性a和b保留了修饰符"?"和readonly。

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

2.4.3、添加和移除修饰符

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

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

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

ts 复制代码
{ -readonly [P in keyof T]-?: T[P] }

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

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

ts 复制代码
type Required<T> = { [P in keyof T]-?: T[P] };

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

ts 复制代码
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类型。

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

ts 复制代码
type ReadonlyPartial<T> = {
    +readonly [P in keyof T]+?: T[P]
};

// 等同于:

type ReadonlyPartial<T> = {
    readonly [P in keyof T]?: T[P]
};
2.4.4、同态映射对象类型深入

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

ts 复制代码
type HMOT<T, X> = { [P in keyof T]: X };

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

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

ts 复制代码
type HMOT<T, X> = { [P in keyof T]: X };

type T = string;

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

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

ts 复制代码
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>"也为数组类型。示例如下:

ts 复制代码
type HMOT<T, X> = { [P in keyof T]: X };

type T = number[];

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

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

ts 复制代码
type HMOT<T> = { [P in keyof T]: T[P] };

type T = number[];

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

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

ts 复制代码
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的成员类型。示例如下:

ts 复制代码
type HMOT<T> = { [P in keyof T]: T[P] };

type T = readonly number[];

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

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

ts 复制代码
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中对应成员的类型。示例如下:

ts 复制代码
type HMOT<T> = { [P in keyof T]: T[P] };

type T = [string, number];

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

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

ts 复制代码
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中对应成员的类型。示例如下:

ts 复制代码
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>"的结果类型为只读数组类型或只读元组类型。示例如下:

ts 复制代码
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>"的结果类型为非只读数组类型或非只读元组类型。示例如下:

ts 复制代码
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];

3、条件类型

条件类型与条件表达式类似,它表示一种非固定的类型。条件类型能够根据条件判断从可选类型中选择其一作为结果类型。

3.1、条件类型的定义

条件类型的定义借用了JavaScript语言中的条件运算符,语法如下所示:

ts 复制代码
T extends U ? X : Y

在该语法中,extends是关键字;T、U、X和Y均表示一种类型。若类型T能够赋值给类型U,则条件类型的结果为类型X,否则条件类型的结果为类型Y。条件类型的结果类型只可能为类型X或者类型Y。

例如,在下例的T0类型中,true类型能够赋值给boolean类型,因此T0类型的结果类型为string类型。在下例的T1类型中,string类型不能赋值给boolean类型,因此T1类型的结果类型为number类型:

ts 复制代码
// string
type T0 = true extends boolean ? string : number;

// number
type T1 = string extends boolean ? string : number;

此例中的条件类型实际意义很小,因为条件类型中的所有类型都是固定的,因此结果类型也是固定的。在实际应用中,条件类型通常与类型参数结合使用。例如,下例中定义了泛型类型别名"TypeName<T>"​,它有一个类型参数T。​"TypeName<T>"的值为条件类型,在该条件类型中根据不同的实际类型参数T将返回不同的类型。示例如下:

ts 复制代码
type TypeName<T> = T extends string
    ? 'string'
    : T extends number
    ? 'number'
    : T extends boolean
    ? 'boolean'
    : T extends undefined
    ? 'undefined'
    : T extends Function
    ? 'function'
    : 'object';

type T0 = TypeName<'a'>;         // 'string'
type T1 = TypeName<0>;           // 'number'
type T2 = TypeName<true>;        // 'boolean'
type T3 = TypeName<undefined>;   // 'undefined'
type T4 = TypeName<() => void>;  // 'function'
type T5 = TypeName<string[]>;    // 'object'

此例中,若实际类型参数T能够赋值给string类型,则"TypeName<T>"表示字符串字面量类型"'string'"​;若实际类型参数T能够赋值给number类型,则"TypeName"表示字符串字面量类型"'number'"​,以此类推。

3.2、分布式条件类型

在条件类型"T extends U ? X : Y"中,如果类型T是一个裸(Naked)类型参数,那么该条件类型也称作分布式条件类型。下面我们先了解一下什么是裸类型参数。

3.2.1、裸类型参数

从字面上理解,裸类型参数是指裸露在外的没有任何装饰的类型参数。如果类型参数不是复合类型的组成部分而是独立出现,那么该类型参数称作裸类型参数。

例如,在下例的"T0<T>"类型中,类型参数T是裸类型参数;但是在"T1<T>"类型中,类型参数T不是裸类型参数,因为它是元组类型的组成部分。因此,类型"T0<T>"是分布式条件类型,而类型"T1<T>"则不是分布式条件类型。示例如下:

ts 复制代码
type T0<T> = T extends string ? true : false;
//           ~
//           裸类型参数

type T1<T> = [T] extends [string] ? true : false;
//            ~
//            非裸类型参数
3.2.2、分布式行为

与常规条件类型相比,分布式条件类型具有一种特殊的行为,那就是在使用实际类型参数实例化分布式条件类型时,如果实际类型参数T为联合类型,那么会将分布式条件类型展开为由子条件类型构成的联合类型。

例如,有如下分布式条件类型,其中T是类型参数:

ts 复制代码
T extends U ? X : Y;

如果实际类型参数T是联合类型"A | B"​,那么分布式条件类型会被展开。示例如下:

ts 复制代码
T ≡ A | B

T extends U ? X : Y
    ≡ (A extends U ? X : Y) | (B extends U ? X : Y)

我们前面介绍的"TypeName"条件类型是分布式条件类型,因为其类型参数T是一个裸类型参数。因此,​"TypeName"类型具有分布式行为。示例如下:

ts 复制代码
type TypeName<T> = T extends string
    ? 'string'
    : T extends number
    ? 'number'
    : T extends boolean
    ? 'boolean'
    : T extends undefined
    ? 'undefined'
    : T extends Function
    ? 'function'
    : 'object';

type T = TypeName<string | number>; // 'string' | 'number'

此例第13行,我们使用联合类型"string | number"来实例化泛型类型别名"Type-Name<T>"​,它表示的分布式条件类型会被展开为联合类型"TypeName | Type-Name<number>"​,因此最终的结果类型为联合类型"'string' | 'number'"​。

3.2.3、过滤联合类型

在了解了分布式条件类型的分布式行为后,我们可以巧妙地利用它来过滤联合类型。在联合类型一节中介绍过,在联合类型"U = U0 |U1"中,若U1是U0的子类型,那么联合类型可以化简为"U = U0"​。例如,下例中的true类型和false类型都是boolean类型的子类型,因此联合类型最终可以化简为boolean类型:

ts 复制代码
boolean | true | false ≡ boolean

never类型是尾端类型,它是任何其他类型的子类型。因此,当never类型与其他类型组成联合类型时,可以直接将never类型从联合类型中"消掉"​。示例如下:

ts 复制代码
T | never ≡ T

基于分布式条件类型和以上两个"公式"​,我们就能够从联合类型中过滤掉特定的类型。例如,下例中的"Exclude<T, U>"类型能够从联合类型T中删除符合条件的类型:

ts 复制代码
type Exclude<T, U> = T extends U ? never : T;

在分布式条件类型"Exclude<T, U>"中,若类型T能够赋值给类型U,则返回never类型;否则,返回类型T。这里巧妙地使用了never类型来从联合类型T中删除符合条件的类型。下面我们来详细分析"Exclude<T, U>"类型的实例化过程。示例如下:

ts 复制代码
T = Exclude<string | undefined, null | undefined>

  = (string extends null | undefined ? never : string)
    |
    (null extends null | undefined ? never : null)

  = string | never

  = string

在了解了"Exclude<T, U>"类型的工作原理后,我们能够很容易地创建一个与之相反的"Extract<T, U>"类型。该类型能够从联合类型T中挑选符合条件的类型。若类型T能够赋值给类型U,则返回T类型;否则,返回类型never。示例如下:

ts 复制代码
type Extract<T, U> = T extends U ? T : never;

type T = Extract<string | number, number | boolean>;
//   ~
//   类型为number

如果"Exclude<T, U>"类型中的类型参数U为联合类型"null |undefined"​,那么"Exclude<T, U>"类型就表示从联合类型T中去除null类型和undefined类型,也就是将类型T转换为一个非空类型。

我们也可以直接创建一个非空类型"NonNullable"​,示例如下:

ts 复制代码
type NonNullable<T> = T extends null | undefined ? never : T;

事实上,上面介绍的三种分布式条件类型"Exclude<T,U>"​"Extract<T, U>"​"Non-Nullable"都是TypeScript语言内置的工具类型。在TypeScript程序中允许直接使用而不需要自己定义。关于工具类型的详细介绍请参考6.8节。

3.2.4、避免分布式行为

分布式条件类型的分布式行为通常是期望的行为,但也可能存在某些场景,让我们想要禁用分布式条件类型的分布式行为。这就需要将分布式条件类型转换为非分布式条件类型。一种可行的方法是将分布式条件类型中的裸类型参数修改为非裸类型参数,这可以通过将extends两侧的类型包裹在元组类型中来实现。这样做之后,原本的分布式条件类型将变成非分布式条件类型,因此也就不再具有分布式行为。例如,有以下的分布式条件类型:

ts 复制代码
type CT<T> = T extends string ? true : false;

type T = CT<string | number>; // boolean

可以通过如下方式将此例中的分布式条件类型转换为非分布式条件类型:

ts 复制代码
type CT<T> = [T] extends [string] ? true : false;

type T = CT<string | number>; // false

3.3、infer关键字

条件类型的语法如下所示:

ts 复制代码
T extends U ? X : Y

在extends语句中类型U的位置上允许使用infer关键字来定义可推断的类型变量,可推断的类型变量只允许在条件类型的true分支中引用,即类型X的位置上使用。示例如下:

ts 复制代码
T extends infer U ? U : Y;

此例中,使用infer声明定义了可推断的类型变量U。当编译器解析该条件类型时,会根据T的实际类型来推断类型变量U的实际类型。示例如下:

ts 复制代码
type CT<T> = T extends Array<infer U> ? U : never;

type T = CT<Array<number>>;    // number

此例中,条件类型"CT"定义了一个可推断的类型变量U,它表示数组元素的类型。第3行,当使用数组类型"Array"实例化"CT"条件类型时,编译器将根据"Array"类型来推断"Array"类型中类型变量U的实际类型,推断出来的类型变量U的实际类型为number类型。

接下来再来看一个例子,我们可以使用条件类型和infer类型变量来获取某个函数的返回值类型。该条件类型"ReturnType"的定义如下所示:

ts 复制代码
type ReturnType<
    T extends (...args: any) => any
> = T extends (...args: any) => infer R ? R : any;

"ReturnType<T>"类型接受函数类型的类型参数,并返回函数的返回值类型。示例如下:

ts 复制代码
type F = (x: number) => string;

type T = ReturnType<F>;    // string

实际上,​"ReturnType<T>"类型是TypeScript语言的内置工具类型。在TypeScript程序中可以直接使用它。

在条件类型中,允许定义多个infer声明。例如,下例中存在两个infer声明,它们定义了同一个推断类型变量U:

ts 复制代码
type CT<T> =
    T extends { a: infer U; b: infer U } ? U : never;

type T = CT<{ a: string; b: number }>; // string | number
相关推荐
吴声子夜歌2 小时前
TypeScript——局部类型、联合类型、交叉类型
javascript·git·typescript
i建模11 小时前
安装 `openclaw@2026.3.1` 时遇到了 Git 仓库访问权限问题
git
枫桥骤雨11 小时前
我的龙虾日记
ubuntu·openclaw
freewlt12 小时前
git配置文件名称大小写
git
枫桥骤雨15 小时前
本地OpenClaw部署教程
ubuntu·openclaw
小熊熊知识库15 小时前
Git工具使用
git
春夜喜雨17 小时前
Git 分支merge合并常用步骤与命令操作
git
秦时明月之君临天下17 小时前
Git查看分支创建时间
git