loading

TS - 条件类型(Conditional Types)、infer

什么是条件类型

条件类型可以让程序根据输入的类型来决定输出的类型是什么,也就是说根据不同的输入类型来确定输出的类型。条件类型的形式有点类似于 JS 中的条件表达式(condition ? trueExpression : falseExpression):

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

tip:[start]在 TS 的条件类型中,符合 ? 问号左边的条件之后,进入第一个分支,TrueType 叫作 true 分支;进入第二个分支,FlaseType 叫作 false 分支。tip:[end]

extends 含义

在条件类型中(Conditional Types)我把 extends 理解为“是否等于”、“是否属于”的意思。

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

在上面的一个条件类型中,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 类型,它们没有地通过 extends 关键字指明两者的关系。

所以,我得到一个结论:在 TypeScript 中无论接口是否通过 extends 指明继承关系,只要 Dog 接口包括 Animal 接口的全部内容,就可以把 Dog 视作 Animal 的子。

war:[start]上述结论仅限于条件类型中。在 extends 关键字中判断左侧是否属于右侧时,如果右侧接口的属性包含了左侧接口所有的属性,它们就存在从属或继承关系。war:[end]

条件类型

提取数组元素类型

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

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

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

在明白了 extends 的意思了之后,条件类型还可以提取数组元素的类型,具体做法如上所示。

判断泛型 T 是否属于数组类型(可能是数字数组、可能是字符串数组,也可能是多种类型数组)。如果 T 属于数组类型,返回 T[number],它索引数组的元素,并获得元素的类型,所以,Str 是一个 string 类型。

这里有必要单独说一说 T[number]。在实际的代码中,索引一个数组的元素时,索引值是一个数字类型,如下所示:

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

const arr = [1, 2, 3];

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

把这些具体的、实际的表达抽象成 TS 类型,那就是 T[number]。得到的数组元素之后就可以知道类型了,如下图所示:

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

tip:[start]T[number] 实际上是索引访问类型(Indexed Access Types),具体查看官方文档:Indexed Access Types。tip:[end]

优化函数重载

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 提取元素类型]
del:[type Flatten<T> = T extends unknown[] ? T[number] : T;]
add:[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
posted @ 2023-07-09 19:46  Himmelbleu  阅读(3)  评论(0编辑  收藏  举报