开始之前
很多刚开始练习类型体操的同学可能不知道哪里可以系统学习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'
。
希望本篇文章对大家有帮助。
后续我们将继续通过其他题目来练习类型体操,一起加油吧!