条件类型

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

条件类型的定义

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

T extends U ? X : Y

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

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

// string
type T0 = true extends boolean ? string : number;

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

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

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<T> 表示字符串字面量类型 'number',以此类推。

分布式条件类型

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

裸类型参数

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

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

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

type T1<T> = [T] extends [string] ? true : false;
//            ~
//            非裸类型参数

分布式行为

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

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

T extends U ? X : Y;

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

T ≡ A | B

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

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

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<string> | Type-Name<number>,因此最终的结果类型为联合类型 'string' | 'number'

过滤联合类型

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

boolean | true | false ≡ boolean

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

T | never ≡ T

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

type Exclude<T, U> = T extends U ? never : T;

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

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。示例如下:

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<T>,示例如下:

type NonNullable<T> = T extends null | undefined ? never : T;

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

避免分布式行为

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

type CT<T> = T extends string ? true : false;

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

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

type CT<T> = [T] extends [string] ? true : false;

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

infer 关键字

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

T extends U ? X : Y

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

T extends infer U ? U : Y;

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

type CT<T> = T extends Array<infer U> ? U : never;

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

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

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

type ReturnType<
    T extends (...args: any) => any
> = T extends (...args: any) => infer R ? R : any;

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

type F = (x: number) => string;

type T = ReturnType<F>;    // string

实际上,ReturnType<T> 类型是 TypeScript 语言的内置工具类型。在 TypeScript 程序中可以直接使用它。关于工具类型的详细介绍请参考 6.8 节。

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

type CT<T> =
    T extends { a: infer U; b: infer U } ? U : never;

type T = CT<{ a: string; b: number }>; // string | number

同时,在多个 infer 声明中也可以定义不同的推断类型变量。例如,下例中的两个 infer 声明分别定义了两个推断类型变量 MN

type CT<T> =
    T extends { a: infer M; b: infer N } ? [M, N] : never;

type T = CT<{ a: string; b: number }>; // [string, number]