前言
对于联合类型在类型系统中的一些特性和行为,开始时一直不甚清楚,模模糊糊的用着。初见联合类型的分配特性是在写MyExclude
这个题的时候,题目要求是从前一个联合类型中,剔除后一个联合类型的值:
Equal<MyExclude<"a" | "b" | "c", "a">, "b" | "c">
初见的时候,没什么思路,当时已经马马虎虎会用 extends
条件表达式了,但是不太清楚联合类型在条件表达式中的处理过程,经过一番折腾,算是渐渐理解了联合类型在遇到条件表达式时候的行为。
下面我用几个题目,来联合类型分配的特性。
分配行为的初理解:43-easy-exclude
题目简述
就如前言中描述,从前一个联合类型中,删除掉后面联合类型中的值。
思路
我们和上一篇文章一样,首先还是从JS的思路来考虑,基本解决的办法就是两个循环,依次去匹配对应的值来进行筛选:
function exclude(arr1, arr2) {
let res = []
for(let val of arr1) {
if (!arr2.includes(val)) {
res.push(val)
}
}
return res
}
所以我们转换成TS的大概思路就是,循环第一个联合类型中的值,然后依次去和第二个联合类型匹配,如果有,就不要,没有,就保留。
那么这里需要解决几个问题:
- 遍历联合类型
- 判断一个值是否在联合类型里
- 累积保存结果
遍历联合类型
这里就要提到官方文档上面对于联合类型在条件语句中的行为描述:
Distributive Conditional Types
When conditional types act on a generic type, they become distributive when given a union type.
大致意思就是,当在条件语句中的泛型的值,被赋予联合类型时,这时就会发生分配行为,举个例子:
type ToArray<Type> = Type extends any ? Type[] : never;
type StrArrOrNumArr = ToArray<string | number>;
// type StrArrOrNumArr = string[] | number[]
从上面的例子可以看到,联合类型在面对extends
时,没有被当做一个整体(如果是的话,那么结果应该是 (string | number)[]
),而是被分别拆开,就像遍历一样,依次取出值进行运算,所以上面的例子运算的时候实际可以类比:
type StrArrorNumArr =
(string extends any ? string[] : never) |
(number extends any ? number[] : never)
从这里也可以知道最后一点累积保存结果的解决方案,联合类型的分配行为之后,会把所有的结果最后做一个联合,正好解决了这个问题。
所以我们循环第一个联合类型:
type MyExclude<T, U> = T extends any ? xxx : xxx
判断一个值是否在联合类型内
涉及判断,这里我们自然还是使用条件语句extends
进行操作,A extends B
是在判断A是否可以赋值给B,简单理解就是A是子类,B是父类,子类可以赋值给父类,就能进肯定分支。
这里父类其实是更宽泛于子类的,所以套到联合类型上来也是一样,更加宽泛的联合类型做B,可以用来判断被它包含的元素:
'a' extends 'a' | 'b' // true
'a' | 'c' extends 'a' | 'c' | 'b' // true
'c' extends 'a' | 'b' // false
所以我们只要能循环出第一个联合类型中的元素,就可以很轻松的判断它是否在第二个联合类型中。结合第一小节中循环:
type MyExclude<T, U> = T extends U ? never : T
这里由于T泛型是联合类型,发生分配,分别对第二个联合类型进行条件判断,如果存在,结果就是never,不是(意味着不在第二个联合类型中),所以得到 T。
最后得到类似 T1 | T2 | never = T1 | T2 的结果。
type MyExclude<'a' | 'b', 'a'>
=
('a' extends 'a' ? never : 'a') | // never
('b' extends 'a' ? never : 'b') // 'b'
= never | 'b'
= 'b'
分配发生的条件
我们来看下这个例子,通过上面例题的描述,我们知道联合类型在遇到extends
条件语句的时候,会发生分配行为。但是在上图的例子中,可以明显看到,temp
的结果,符合我们对于联合类型分配行为的预期;而temp1
,在表达式完全和 Exclude$
等同的情况下,结果却完全不符合联合类型分配行为之后的结果:
// 预期的分配行为
type res = 'name' | 'age' | 'gender' extends 'name' | 'age' ? never : T
= ('name' extends 'name' | 'age' ? never : 'name') |
('age' extends 'name' | 'age' ? never : 'age') |
('gender' extends 'name' | 'age' ? never : 'gender')
= never | never |'gender'
= 'gender'
这是为什么呢?当时我也很疑惑,其实这里是因为错误的判读了文档的内容,让我们再回头看下TS官方文档对于分配行为的原文描述:
When conditional types act on a generic type, they become distributive when given a union type.
我们可以注意到,在文档中描述联合类型分配行为的时候,不仅提到了在条件语句中,还有一句“When conditioncal types act on a generic type”,这里的意思是,当条件作用于泛型的时候。
所以这里文档描述的是,当extends
两头是泛型进行比较的时候,给泛型的值如果是联合类型,才会发生分配行为。反之,如果extends
两头不是泛型,那么即使是联合类型,也不会发生分配,就当做一整个值来使用了,所以上面的例子中,temp1
的结果才会是那样。
空的联合类型never:1042-medium-isnever
题目简述
判断输入值是否是never
type res = IsNever<1> // false
type res1 = IsNever<never> // true
思路
看起来似乎非常简单,由于never
的特性,只有never
值,才可以赋值给never
类型;结合我们常用的条件判断语句extends
的本质:T1 extends T2
其实是在判断 T1
能否赋值给 T2
。
所以很容易写下解法:
type IsNever<T> = T extends never ? true : false
但是,写完之后,你会发现,它并不如你想的那般工作:
type res = IsNever<never> // never
你发现本应该结果为true
的输入值得到了never
的结果。这是非常奇怪的结果,也就是说当输入值是never的时候,extends既没有进肯定分支(true),也没有进否定分支(false)。
这是为什么呢,在github上找到了讨论类似问题的issue,下面有官方的回答:
never is essentially an empty union type, and using union types with conditional type inference results in distributive behaviour. Distributing over an empty union results in an empty union, as there’s nothing to distribute over.
大致意思就是,never
本质上是一个空的联合类型,所以当它赋值给泛型,就会在条件语句中发生分配行为,此时空联合类型自然是无法分配的,所以结果是never
。
这也解释了分配行为后,结果联合在一起的时候,never
会被忽视的现象:
'a' | never = 'a'
所以,为了避免这种不可预期的分配行为,我们将泛型比较的时候放入数组进行规避,这种写法也是在官方文档中的写法:
type IsNever<T> = [T] extends [never] ? true : false
结语
通过这篇文章,应该是总结了我目前学习TS到现在,遇到的所有关于联合类型的特性。希望我在后面能够更加熟练运用以解决其它问题。
永远进步