TypeScript 条件类型(Conditional Types)以及 infer 关键字

发布时间 2023-07-09 19:51:44作者: Himmelbleu

什么是条件类型

条件类型可以让程序根据输入的类型来决定输出的类型是什么,也就是说根据不同的输入类型来确定输出的类型。

条件类型的形式有点类似于 JS 中的条件表达式(condition ? trueExpression : falseExpression):

file:[条件类型的规则]
SomeType extends OtherType ? TrueType : FalseType;

extends 含义

我直接把 extends 理解成继承(或从属)的意思。在条件类型中(Conditional Types)我甚至理解为是否等于的意思。

file:[extends 的含义]
interface Animal {
    name: string;
    age: number;
}

// Dog 接口继承于 Animal
interface Dog extends Animal {
    run: () => void;
}

// Dog 是否等于 Animal,或者 Dog 是否属于 Animal
type A = Dog extends Animal ? string : number
//   ^? string

在上面的一个条件类型中,很明显符合我认定的 extends 含义,Dog 是否属于 Animal,如果属于就返回 string,否则返回 number 类型。

反直觉的 extends

Animal 和 Dog 两个接口表面上没有任何的继承关系,相同点在于它们都有 name、age 属性且类型相同。

file:[反直觉的 extends]
interface Animal {
    name: string;
    age: number;
}

interface Dog {
    name: string;
    age: number;
    run: () => void;
}

type A = Dog extends Animal ? string : number
//   ^? string

A 得到的是一个 string 类型,这不说明 Dog 还是继承了 Animal 吗?但是,它们没有明确地通过 extends 关键字说明两者的关系。

因此,我得到一个结论:在 TypeScript 中无论接口是否通过 extends 指明继承关系,只要 Dog 接口包括 Animal 接口的全部内容,就可以被视作继承关系。但仅限于条件类型中。

条件类型

提取数组元素类型

file:[提取数组元素的类型]
type Flatten<T> = T extends unknown[] ? T[number] : T;

type Str = Flatten<string[]>;
    //^? string

type Num = Flatten<number>;
    //^? number

Flatten 判断泛型 T 是否属于数组类型,如果属于数组类型,就返回 T[number],它索引数组元素,并获得元素的类型。

这里很抽象的是 T[number],在具体的代码中我们索引一个数组的元素时,索引签名是数字,比如:

file:[索引数组类型的元素 并获取元素的值]

const arr = [1, 2, 3];

const item = arr[0]; // item => 1

在 TS 类型中也是一个道理,我们索引值 0 是一个具体的值,在 TS 类型中表示 number,把具体的代码抽象成 TS 来表示就是:T[number]。这个泛型 T 代表数组的数组类型,可能是数字数组、布尔数组、字符串数组。如下图所示,TS 类型与上面的具体代码:

索引 TS 数组类型的元素,并获取元素的类型

所以,类型 Flatten 就可以提取数组中元素的类型,并返回元素类型。

优化函数重载

file:[函数重载普通写法]
interface IdLabel {
  id: number;
}

interface NameLabel {
  name: string;
}

function createLabel(id: number): IdLabel;
function createLabel(name: string): NameLabel;
function createLabel(nameOrId: string | number): IdLabel | NameLabel;
function createLabel(nameOrId: string | number): IdLabel | NameLabel {
  throw "unimplemented";
}

如果 createLabel 函数每一种情况都发生了变化,重载数量呈指数增长。取而代之的是,通过条件类型:

file:[通过条件类型优化函数重载]
type NameOrId<T extends number | string> = T extends number
  ? IdLabel
  : NameLabel;


function createLabel<T extends number | string>(idOrName: T): NameOrId<T> {
  throw "unimplemented";
}

我们可以使用这个条件类型将我们的重载简化为一个没有重载的单个函数。

infer

infer 提取数组元素类型

引入 infer 关键字,优化上面的 Flatten 类型别名,如下所示:

file:[infer 提取元素类型]
type Flatten<T> = T extends Array<infer I> ? I : T;

type Str = Flatten<string[]>
    //^? string

两者之间大差不差,区别就是,多了一个泛型 I,这个泛型 I 的最终结果由 infer 推断而来。

当传递 string[] 时,infer 关键字判断这个数组的元素类型是什么,很明显是 string 类型。因此,泛型 I 就是 string,而这个条件类型中符合 true 分支,所以,Flatten 返回的结果是泛型 I。

infer 提取函数返回值类型

在明白了 infer 关键字的意思之后,除了上述的作用以外,还可以提取函数的返回值类型。

file:[infer 提取函数返回值类型]
type GetReturnType<T> = T extends (...args: never[]) => infer R ? R: never;

type F1 = GetReturnType<() => string>;
    //^? string

type F2 = GetReturnType<(x: string, y: number) => number[]>;
   //^? number[]

infer R 中,目前不知道 R 的具体类型。我在 GetReturnType<() => string> 中传递了一个函数,其返回类型是 string。infer 就知道了 R 是一个 string 类型,所以 F1 就是 string 类型。

分配条件类型

file:[分配条件类型]
type A1 = 'x' extends 'x' ? string : number;
//   ^? string
type A2 = 'x' | 'y' extends 'x' ? string : number;
//   ^? number

type P<T> = T extends 'x' ? string : number;
type A3 = P<'x' | 'y'>
//   ^? string | number

在遇到联合类型的时候,条件类型和上面所展示的结果会不一样,具体表现在,extends 中左边如果是一个联合类型的时候,最终得到的是 false 条件的结果。

如上所示,A2 的结果是 number 类型,而不是 string 类型,extends 左侧是一个联合类型 'x' | 'y',明显都是 string 类型,结果却相反。

但是,当如果 extends 左侧是一个泛型,而在使用时给泛型传递的是一个联合类型,结果又不一样。

如上所示,定义了一个 P<T>,在 A3 中,我给 P 的泛型 T 传递的是一个联合类型,最终的结果是 string | number

阻止分配条件类型

针对以上的情况,在给泛型 T 传递联合类型时,在 extends 左侧使用 [] 把泛型包裹起来,就可以阻止结果是一个联合类型。

file:[阻止分配条件类型]
type P<T> = [T] extends ['x'] ? string : number;
type A3 = P<'x' | 'y'>
//   ^? number

never 的分配条件类型

file:[never 类型]
type A1 = never extends 'x' ? string : number;
//   ^? string

type P<T> = T extends 'x' ? string : number;
type A2 = P<never>
//   ^? never

never 类型可以是任何类型的子类型。因此,A1 是 string 类型。

同样的,我们给 extends 左侧是一个泛型,传递一个 never 给泛型,结果就是 never。如下所示,如果不想结果是一个 never,把泛型 T 包裹起来,结果就不是 never,而是 string。

file:[阻止结果是 never 类型]
type P<T> = [T] extends ['x'] ? string : number;
type A2 = P<never>
//   ^? string