Skip to main content

TypeScript 中的 extends 与 infer

· 10 min read

介绍 extends 条件类型用法和 infer 搭配进行类型推断

extends 条件类型

基本语法

extends 条件类型有点类似三元运算符,下面来看一下它的基本用法。

interface Animal {
live(): void;
}
interface Dog extends Animal {
woof(): void;
}

// Dog 类型是 Animal 的子类型?true 则返回 number,否者返回 string
type Example1 = Dog extends Animal ? number : string; // number

// RegExp 类型是 Animal 的子类型?true 则返回 number,否者返回 string
type Example2 = RegExp extends Animal ? number : string; // string
important

判断一个对象类型是否为另一个对象类型的子类型,只需要看这个类型是否涵盖了父类型的所有属性。

因此,子类型 的属性数量一定是大于或者等于 父类型 的属性数量的。

条件类型的强大之处在于配合泛型一起使用,下面来看一个案例。

interface IdLabel {
id: number /* some fields */;
}
interface NameLabel {
name: string /* other fields */;
}

function createLabel(id: number): IdLabel;
function createLabel(name: string): NameLabel;
function createLabel(nameOrId: string | number): IdLabel | NameLabel;
function createLabel(nameOrId: string | number): IdLabel | NameLabel {
if(typeof nameOrId === 'number') return {id: nameOrId};
else return {name: nameOrId};
}

这里我们使用了 TS 中的 函数重载 机制,可以根据传入参数的类型来决定返回值。

let a = createLabel(1); // IdLable
let b = createLabel("1"); // NameLable
let c = createLabel(Math.random() ? "hello" : 42); // IdLable | NameLable

实际上我们可以使用 extends 条件类型来简化函数重载。

// T 如果是 number 类型则返回 IdLabel 类型,否者返回 NameLabel 类型
type NameOrId<T extends number | string> = T extends number ? IdLabel : NameLabel;

function createLabel<T extends number | string>(nameOrId: T): NameOrId<T> {
return (typeof nameOrId === "number") ? {id: nameOrId} as NameOrId<T> : {name: nameOrId} as NameOrId<T>;
}

let a = createLabel(1); // IdLable
let b = createLabel("1"); // NameLable
let c = createLabel(Math.random() ? "hello" : 42); // IdLable | NameLable

条件类型约束

先看一个泛型约束的基本用法例子。

// 获取对象类型中 message 属性的类型
type MessageOf<T extends { message: unknown }> = T["message"];

interface Email {
message: string;
}

type EmailMessageContents = MessageOf<Email>;

我们使用 MessageOf 时,要求传入的泛型参数必须带有 message 属性,否者会编译报错。

现在实现一个需求:让 MessageOf 的泛型参数可以为任意类型,具有 message 属性则返回该属性的类型,不具有 message 属性则返回 never 类型。这个需求可以使用条件泛型约束实现。

type MessageOf<T> = T extends { message: unknown } ? T["message"] : never;

条件类型在联合类型中的使用

上面所有的例子都是对象类型和条件类型搭配使用,还有一个常见的用法就是在联合类型中使用条件类型。

T extends U ? X : Y

这里的 TU 都是联合类型,把联合类型看作是一个集合,整个过程中会把 T 中的每一个子类型拿出来和 U 集合进行比较,判断 T 中的子类型是否为 U 集合的子集,如果是则返回 X,否者返回 Y,最终会把所有返回的结果重新进行联合。

type P<T> = T extends 0 | 1 ? "A" : "B";

type A = P<0 | 1 | 2>; // type A = "A" | "B";

整个过程就是:

  • 判断 0 是否为 0 | 1 的子集,返回 "A"
  • 判断 1 是否为 0 | 1 的子集,返回 "A"
  • 判断 2 是否为 0 | 1 的子集 ,返回 "B"
  • 最后将所有返回结果联合起来,"A" | "A" | "B""A" | "B"
important

上面的过程 当且仅当使用泛型时,联合类型的条件判断才会具有一个分发的过程,如果把 T 写为一个固定的联合类型:

type P = 0 | 1 | 2 extends 0 | 1 ? "A" : "B"; // type A = "B"

此时会把 0 | 1 | 20 | 1 都看作一个整体,直接判断 0 | 1 | 2 是否为 0 | 1 的子集,如果是则返回 "A",否者返回 "B",所以这里的类型 P 实际上是一个字面量类型 "B"

使用泛型是如果想要避免分发过程,将泛型参数看作一个整体,可以加上 []

type P<T> = [T] extends 0 | 1 ? "A" : "B";

type A = P<0 | 1 | 2>; // type A = "B";

下面我们来实现几个应用,利用条件类型实现联合类型的交集、差集。

先明确一下概念:

  • 交集:T 中的元素能在 U 集合中找到,所有的这些元素形成的集合就是 TU 的交集
  • 差集:T 中的元素能在 U 集合中找到,从 T 集合中去掉这些元素后就是 TU
type Filter<T, U> = T extends U ? T : never; // 交集
type Diff<T, U> = T extends U ? never : T; // 差集

TS 中也提供了这两种需求的泛型工具类型:Extract 交集Exclude 差集

使用 infer 在条件类型中推断

基本语法

我们先来实现一个需求,获取传入的数组类型的元素类型。

type Flatten<T> = T extends unknown[] ? T[number] : T;

let num: Flatten<number[]> = 1; // num 是 number 类型

这里我们使用了数组的类型索引机制 T[number],可以返回数组的元素类型。 下面我们通过 infer 来实现这个需求。

type Flatten<T> = T extends Array<infer R> ? R : T;

这里的 infer R 相当于在条件类型中定义了一个新的类型 R,这里可以理解为如果 T 是一个数组类型,那么就会将该数组类型的元素类型赋值给 R,然后返回 R,否者返回 T

important
T extends U ? X : Y

infer 语句只能在条件类型 extendsU 位置处使用,作用是定义一个类型 R

如果条件满足则会根据类型 UR 赋值,上面的例子中是根据 T = Array<R> 这个表达式,给 R 类型赋值的,比如这里 T 如果是 string[],那么 R 就会被赋值为 string 类型。

还需要记住一点,R 只能在条件判断为 true 的分支中使用。

分析 ReturnType 与 Parameters

TS 官方提供了有关函数类型的两个泛型工具类型:

  • ReturnType<T>,用于获取一个函数类型的返回值类型
  • Parameters<T>,用于获取一个函数类型的所有参数类型,最后将参数类型作为元组返回

这两个泛型工具类型很典型的使用了 extends 条件类型与 infer 类型推断,下面来分析一下它们的源码。

ReturnType

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

在泛型参数列表中使用 T extends (...args: any[]) => any 约束泛型参数必须是一个函数类型,然后再通过 T extends (...args: any[]) => infer R,将函数类型 T 的返回值类型赋值给 infer R,最后返回 R

Parameters

type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;

在泛型参数列表中使用 T extends (...args: any) => any 约束泛型参数必须是一个函数类型,然后再通过 T extends (...args: infer P) => any,利用 函数的剩余参数 将所有参数类型封装到一个元组中并赋值给 infer P,最后返回 P

Reference