本文的起因是在做类型体操时出现了一些诡异的行为。题目链接在这,有兴趣的朋友可以看看题目,最后会来类型体操一下。
问题
简单描述这个诡异的行为就是
type a = never extends never ? true : false // true
type case1<T = never> = never extends T ? true : false
type case2<T = never> = T extends never ? true : false
type b = case1 // true
type c = case2 // never
奇怪的地方发现没有,正常情况下,never 是可以 extends never的,见 type a、type b,结果true。但在type c中,T extends never ? true : false理论上会被实例化成never extends never ? true : false ,预期结果是true,但却出现了never。
要解决这个问题。首先就涉及到 extends 是什么含义?
子类型
在Typescript中,如下的代码是可以正常运行的,func接收类型为 TypeA的参数,而obj作为一个类型为TypeB的字面量对象,可以正常传入func中。我们可以称TypeB为TypeA的子类型。
interface TypeA {
a: string;
}
interface TypeB {
a: string;
}
function func(x: TypeA) {}
const obj: TypeB = { a: 'a' };
func(obj) // OK
从extends的角度讲,如果 TypeA 可以 extends TypeB,那么说明TypeA 是 TypeB的子类型(儿子:D)。
名义子类型
在一些其它的编程语言中,要给TypeA创建一个子类型TypeB,必须要显式声明这种父子关系。比如
interface TypeB extends TypeA {}
如果不显式声明,编译器会报错。这就是名义子类型
结构子类型
在Typescript中,不需要显式的声明,只需要结构上相似既可。我们称之为结构子类型。
如TypeA和TypeB,结构完全一致,它们的父子关系是相互的,既可以说TypeA是TypeB的子类型,也可以说TypeB是TypeA的子类型。
如果TypeB改写成如下的形式,那么TypeA就无法成为TypeB的子类型了,也就是说子类型必须不能比父类型缺少属性(可以多)。
interface TypeB {
a: string;
b: string;
}
Conditional Types
Conditional Types指条件类型,长下面这样。当SomeType 是 OtherType的子类型时就会走到TrueType,否则走到FalseType。
SomeType extends OtherType ? TrueType : FalseType;
具体例子
type A = 1 extends 1 ? true : false; // true
但这依然解释不了下面这种情况。明明不论TrueType、FalseType都没有never。
type case2<T = never> = T extends never ? true : false
type c = case2 // never
Distributive Conditional Types
当条件类型在一个泛型类型身上发生时,就会变成Distributive Conditional Types。举个例子
type Distributive<T> = T extends unknown ? [T] : never;
type A = Distributive<string | number>;
按上文的逻辑来推理,去掉泛型后,A可以转化成如下的内容,最后结果是[string | number]
type A = string | number extends unknown ? [string | number] : never;
但实际运行后发现,A的结果是[string] | [number]。
官方文档的说明如下
When conditional types act on a generic type, they become distributive when given a union type.
由于type A = Distributive<string | number>,传入了一个Union Type,因此实际上去掉泛型后,A长这样。
type A = (string extends unknown ? [string] : never) | (number extends unknown ? [number] : never)
要注意的是,触发Distributive Conditional Types的条件如下,以下面的代码为例。
type Distributive<T> = T extends unknown ? [T] : never;
-
泛型参数必须在
extends的左侧,不能在右侧type Distributive<T> = unknown extends T ? [T] : never -
必须是
naked type。可以理解为裸露的类型,通俗来讲,就是说不能对泛型参数做任何的修饰,必须是单纯的T。比如type Distributive<T> = T | 1 extends unknown ? [T] : never中的T | 1已经不单纯了。 -
必须是
Union type
结论
那么如何去解释这段代码呢
type case2<T = never> = T extends never ? true : false
type c = case2 // never
众所周知,never在typescript中指不存在的类型。而Union type代表一个集合,never的意思就是一个空的集合。在Distributive Conditional Types的场景下,extends能够表示遍历集合的含义。而遍历一个空的集合never,自然得到的结果也是空never。
那么有观众就要问了,为什么never也是Union type呢?never其实是|运算的幺元。所有的类型都可以理解为Union type。
比如,type U<T> = T | never,我们会得到U的类型是T,通俗理解就是你拥有了一个类型T,此时还拥有一个不存在的东西never,那么最终你手上有的还只是T。所以T和T | never是恒等的。
那么如何去解决这个问题呢?就是不触发Distributive Conditional Types既可,最简单的方法就是不要让T是naked type。如下的代码,只要我们把T包裹住即可。
type case2<T = never> = [T] extends [never] ? true : false
type c = case2 // true
最后
题目要求传入两个泛型参数,第一个参数是[1, 2, null, 3],第二个参数是null,预期最终把数组的的null去掉,得到[1, 2, 3]。诶这不就是JS里面的filter嘛。于是我们的逻辑很简单,遍历数组中的元素,如果是第二个参数的子类型,就过滤掉,如果不是,就保留。
type FilterOut<T extends any[], F, R extends any[] = []> =
T extends [infer H, ...infer P]
? ([H] extends [F] ? FilterOut<P, F, R> : FilterOut<P, F, [...R, H]>)
: R
我们一步步来解释这段代码
-
首先
T extends [infer H, ...infer P]这个条件类型可以在extends的子句中拿到H和P,H是数组第一个元素,P是数组剩余的元素形成的数组。 -
我们前面初始化了一个空数组
R作为FilterOut的结果,可以理解为JS中的一个局部变量。当H是F的子类型时,即[H]可以extends[F],我们就过滤掉H,对剩余元素P进行处理,走到FilterOut<P, F, R>。否则,我们就把H加入到结果数组R中,走到FilterOut<P, F, [...R, H]>。 -
递归到最后,我们的终止条件是
P为空数组,而P是FilterOut的第一个参数。空数组时,T extends [infer H, ...infer P]失败,直接返回结果数组R。