TypeScript类型体操训练(二)

1,086 阅读4分钟

开始之前

很多刚开始练习类型体操的同学可能不知道哪里可以系统学习TypeScript的操作符,这里我推荐官网的handbook,里面介绍了TypeScript最新版本的所有特性。如果你只想了解类型操作符相关内容,可以只看Type Manipulation章节。

接下来,我们还是通过一起解答题目的方式来学习类型体操。

Exclude

题目来自Type Challenges easy 43,不过这个类型也是TypeScript的内置类型之一。

使用:

type T0 = MyExclude<"a" | "b" | "c", "a">; // "b" | "c"
     
type T1 = MyExclude<"a" | "b" | "c", "a" | "b">; // "c"
     
type T2 = MyExclude<string | number | (() => void), Function>; //string | number

可以看到,Exclude可以从一个联合类型中剔除指定的成员,返回一个新类型。

先分析一下要点:

  • 判断一个成员是否在一个联合类型中
  • 剔除类型

在实现之前,先来了解一下相关的操作符。

extends和三元运算符

在上一章的Pick练习中,我们已经使用了extends操作符来对泛型参数进行约束:

type MyPick<T, K extends keyof T> = {}

可以看到,extends可用于判断联合类型之间的包含关系,它可以直接用于表达泛型参数的约束。

那么,在类型内部如何使用extends呢?

先试试直接使用:

type unionA = 'a' | 'b' | 'c'
type unionB = 'c'

type IsExtends = unionB extends unionA //error!

发现编译器报错了,说明在类型内部不能这样写,这也是初学者容易混淆的一点。

在类型内部,我们应该使用extends + 三元运算符的书写形式:

type IsExtends = unionB extends unionA ? true : false //ok

这样,我们就为两种不同的情况分配了不同的类型。

extends的规则

extends的规则比较复杂,下面将分情况介绍。

通常情况

下面直接介绍两个类型extends时遵循的计算规则:

interface Animal {}
  
interface Dog extends Animal {}

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

对于通常的情况,extends判断条件真假的逻辑很简单,就这么记:

如果extends左边的类型的变量能够赋值给extends右边的类型的变量,那么表达式判断为真,否则为假

在这个例子中,Dog是Animal的子类,子类的限制一定比父类多,所以子类的变量可以赋值给父类的变量,所以这里判断为真。

另一个栗子:

interface A{
    name: string
}
interface B{
    name: string
    age: number
}
type C = B extends A ? string : number //string

很好理解吧!

泛型情况

先来看一个栗子:

type P<T> = T extends 'x' ? string : number

type A = P<'x' | 'y'>  // ?

A是什么?如果你按照上面的规则来,肯定认为是string吧!

然而A的类型是string | number

这个反直觉的结果发生的原因是extends的一个规则:

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

对于使用extends关键字的条件类型(即上面的三元表达式类型),如果extends前面的参数是一个泛型类型,当传入该参数的是联合类型,则使用分配律计算最终的结果

按照分配律来进行,也就是说,上面的A其实是这样的:

type P<T> = T extends 'x' ? string : number

// type A = P<'x' | 'y'>  
type A =  ('x' extends 'x' ? string : number) | ('y' extends 'x' ? string : number)

总结:如果参数是泛型且代入参数的是联合类型时,extends就会使用分配律。

特殊的never

学习了上面的知识,再来看一个例子:

  type A1 = never extends 'x' ? string : number; // string

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

你可能觉得按照规则来,这里应该是不会发生分配律的,所以A1和A2应该相同。

但这里仍然发生了分配律,因为never其实是空的联合类型。所以当开始分配时,编译器发现没有可以分配的类型,那么P<T>的表达式其实就没有执行,所以A2也就类似于永远没有返回的函数一样,它此时就是never类型。

知道了never的特性和extends的分配律,你有没有想到我们最开始讲的剔除元素的解法呢?

解答

学习了extends分配律和never,我们可以获得解答Exclude的思路了:

  • 首先要接受泛型参数,触发分配律
  • 如果元素是应该被剔除的(extends为真),那么就返回never来剔除元素,否则保留

最终答案:

type T0 = MyExclude<"a" | "b" | "c", "a">

type T1 = MyExclude<"a" | "b" | "c", "a" | "b">

type T2 = MyExclude<string | number | (() => void), Function>

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

如果你已经懂了,恭喜,如果还不明白,我们以T0为例来讲一下发生了什么:

type T0 = MyExclude<'a' | 'b' | 'c', 'a'>

我们手动使用分配律来解构一下MyExclude

type T0 = ('a' extends 'a' ? never : 'a') |
		  ('b' extends 'a' ? never : 'b') |
		  ('c' extends 'a' ? never : 'c') 

解得:

type T0 = never | 'b' | 'c' 

never实际为空,T0最终为'b' | 'c'


希望本篇文章对大家有帮助。

后续我们将继续通过其他题目来练习类型体操,一起加油吧!