# 前言

在刚开始学习 TypeScript 的时候,看见关键字 extends 总会与 js 中继承关联,以为它就是继承的意思,但是通过一些学习后发现,extends 在不同的场景下,是有不同的含义,因此总结如下:

  • 表示继承/拓展的含义
  • 表示约束的含义
  • 表示分配的含义

# 继承/拓展

extends 是 ES6 中引入的关键字,用来表示从父类中继承属性和方法。

class Person {
  constructor(name) {
    this.name = name
  }
  say() {
    console.log(`我的名字是${this.name}`)
  }
}

class Child extends Person {
  constructor(name, age) {
    super(name)
    this.age = age
  }
  run() {
    console.log('跑起来!!!')
  }
}

const person = new Child('小红', 18)
person.name // => 小红
person.run() // => 跑起来!!!
person.say() // => 我的名字是小红

<!-- Child 继承了父类的 say 方法,因为可以在 Child 实例 person 上调用-->

ts 中,extends 除了像 js 可以继承值,还可以继承/扩展类型

interface Animal {
  name: string;
}

// 接口 Dog 类型除了本身 say方法外,还继承了 Animal 中的 name 属性,同时拥有了 name,say
interface Dog extends Animal {
  say(): void;
}

// Dog => { name: string, say(): void }

// 声明变量 a 时,就必须要有 name,say,否则 ts 会报错
const a: Dog = { name: '小黑', say() { console.log('汪汪汪')} }

# 泛型约束

在某些泛型使用时,我们往往需要对参数进行一定的限制,比如希望传入的参数都有 id 属性和 render 方法,我们便可以这样来写:

function arrList<T extends { id: number, render(n: number): number}>(arr: T[]): number[] {
  return arr.map(item => (item.render(item.id)));
}

这里的 extends 就对参数作了一个限制,就是 arr 每一项都必须有 id 和 render 方法。

# 条件类型和高阶类型

SomeType extends OtherType ? TrueType : FalseType;

When the type on the left of the extends is assignable to the one on the right, then you’ll get the type in the first branch (the “true” branch); otherwise you’ll get the type in the latter branch (the “false” branch).

大致意思为:当左边的类型可以赋值给右边的类型时,你将得到第一个分支中的类型("真 "分支);否则你将得到后一个分支中的类型("假 "分支)。

exetnds 可以用来判断一个类型是不是可以分配给另一个类型,举个例子:

type Person = { name: string }

type Animal = { name: string }

type a = Animal extends Person ? true : false

// a => true

这是因为 PersonAnimal 的类型完全相同,或者说 Person 类型的一切约束条件,Animal 都具备;换言之,类型为 Person 的值可以分配给类型为 Animal 的值(分配成功的前提是,Animal里面得的类型得有一样的),反之亦然。需要理解的是,这里A extends B,是指类型 A可以分配给类型B,而不是说类型A是类型B的子集

再看一个例子:

type Person = { name: string, age: number }

type Animal = { name: string }

type a = Animal extends Person ? true : false

// a => false

发现此时 afalse,这是因为 Animal 没有类型为 numberage 属性,类型Animal不满足类型 Person 的类型约束。因此,A extends B,是指 类型A可以分配给类型B,而不是说类型A是类型B的子集,理解extends在类型三元表达式里的用法非常重要。

再来继续看:

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'> // ?

如果将 A3 带入到 A2中,形式就是完全一样的,那得到的结果 A3 就和 A2 类型一样吗?

这里先给出结论:

type P<T> = T extends 'x' ? string : number;
type A3 = P<'x' | 'y'>  // A3的类型是 string | number

是不是很反直觉?这个反直觉结果的原因就是所谓的 分配条件类型(Distributive Conditional Types)

When conditional types act on a generic type, they become distributive when given a union type

大致可以理解为:对于使用extends关键字的条件类型(即上面的三元表达式类型),如果extends前面的参数是一个泛型类型,当传入该参数的是联合类型,则使用分配律计算最终的结果。分配律是指,将联合类型的联合项拆成单项,分别代入条件类型,然后将每个单项代入得到的结果再联合起来,得到最终的判断结果。

type P<T> = T extends 'x' ? string : number;
type A3 = P<'x' | 'y'>  // A3的类型是 string | number

// 将 x,y 带入 P<T>

// P<'x' | 'y'> => P<'x'> | P<'y'>

// 'x'代入得到,'x' extends 'x' ? string : number => string

// 'y'代入得到,'y' extends 'x' ? string : number => number

// 然后将每一项代入得到的结果联合起来,得到 string | number

总之,满足两个要点即可适用分配律:第一,参数是泛型类型,第二,代入参数的是联合类型

# 特殊的 never

// never是所有类型的子类型
type A1 = never extends 'x' ? string : number; // string

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

上面的示例中,A2和A1的结果竟然不一样,看起来never并不是一个联合类型,所以直接代入条件类型的定义即可,获取的结果应该和A1一直才对啊?

实际上,这里还是条件分配类型在起作用。never被认为是空的联合类型,也就是说,没有联合项的联合类型,所以还是满足上面的分配律,然而因为没有联合项可以分配,所以P<T>的表达式其实根本就没有执行,所以A2的定义也就类似于永远没有返回的函数一样,是never类型的。

# 防止条件判断中的分配

type P<T> = [T] extends ['x'] ? string : number;
type A1 = P<'x' | 'y'> // number
type A2 = P<never> // string

在条件判断类型的定义中,将泛型参数使用[]括起来,即可阻断条件判断类型的分配,此时,传入参数T的类型将被当做一个整体,不再分配。

# 高级类型应用

  • Exclude

Exclude是TS中的一个高级类型,其作用是从第一个联合类型参数中,将第二个联合类型中出现的联合项全部排除,只留下没有出现过的参数。

示例:

type A = Exclude<'key1' | 'key2', 'key2'> // 'key1'

Exclude的定义是

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

这个定义就利用了条件类型中的分配原则,来尝试将实例拆开看看发生了什么:

type A = `Exclude<'key1' | 'key2', 'key2'>`

// 等价于
type A = `Exclude<'key1', 'key2'>` | `Exclude<'key2', 'key2'>`

// =>
type A = ('key1' extends 'key2' ? never : 'key1') | ('key'2 extends 'key2' ? never : 'key2')

// never是所有类型的子类型
type A = 'key1' | never = 'key1'
  • Extract

高级类型Extract和上面的Exclude刚好相反,它是将第二个参数的联合项从第一个参数的联合项中提取出来,当然,第二个参数可以含有第一个参数没有的项。

下面是其定义和一个例子,有兴趣可以自己推导一下

type Extract<T, U> = T extends U ? T : never
type A = Extract<'key1' | 'key2', 'key1'> // 'key1'
  • Pick

extends的条件判断,除了定义条件类型,还能在泛型表达式中用来约束泛型参数

// 高级类型Pick的定义
type Pick<T, K extends keyof T> = {
  [P in K]: T[P]
}

interface A {
    name: string;
    age: number;
    sex: number;
}

type A1 = Pick<A, 'name'|'age'>
// 报错:类型“"key" | "noSuchKey"”不满足约束“keyof A”
type A2 = Pick<A, 'name'|'noSuchKey'>

Pick的意思是,从接口T中,将联合类型K中涉及到的项挑选出来,形成一个新的接口,其中K extends keyof T则是用来约束K的条件,即,传入K的参数必须使得这个条件为真,否则ts就会报错,也就是说,K的联合项必须来自接口T的属性。

以上就是ts中 extends 关键字的常用场景。