前言
今天我们来做一下一道困难级别的题:Union to Tuple。
题目要求
本题要求将一个联合类型转换为元组类型。
由于元组是有序的,而联合类型是无序的,因此本题并不要求元组的排列顺序,可以任意顺序作为结果。
例如:
UnionToTuple<1>// 要求结果是[1]
UnionToTuple<'a' | 'b'> // 要求结果是['a', 'b']或者['b', 'a'],不能是('a' | 'b')[]
这道题的解题思路倒是不难:我们可以遍历联合类型,然后用递归将每一项逐个添加到元组中。
但如何来实现呢?
在回答这个问题前,我们需要一些前置知识。
前置知识
函数类型的交叉类型
交叉类型是TS的基础知识,但是大部分人可能都没有思考过这个问题:函数类型的交叉类型是什么?例如:
type Fn1 = () => number
type Fn2 = () => string
type Fn12 = Fn1 & Fn2
这里的Fn1
和Fn2
是两个函数类型,返回值类型分别是number
和string
,Fn12
是二者的交叉类型。
请问:Fn12
是什么类型?或者更具体一点:如果Fn12
是一个函数类型,那它的返回值类型是什么?
我们可以借助TS的内置工具类ReturnType
测试一下:
type R = ReturnType<Fn12> // type R = string
Fn12
的返回值类型竟然是string
!也就是Fn2
的返回值类型。
如果交换位置,Fn2 & Fn1
,结果将是number
。
为什么会这样呢?
实际上,函数类型的交叉类型,相当于函数重载,type Fn12 = Fn1 & Fn2
,结果就相当于:
type Fn12 = {
(): number,
(): string,
}
Fn12
是一个包含了两个调用签名(Call Signatures)的函数类型。
如果你不太理解一个函数具有多个调用签名,这里我举个更具体例子:
function add(a: number, b: number): number;
function add(a: string, b: string): string;
function add(a: any, b: any): any {
return a + b;
}
// add函数的类型
type Add = {
(a: number, b: number): number;
(a: string, b: string): string;
};
// Add的返回值类型是strng
type R = ReturnType<Add> // string
// add的类型就是Add
let a: Add = add
// 测试add是否是Add类型
type t1 = Add extends typeof add ? true : false; // true
type t2 = typeof add extends Add ? true: false; // true
这里的add
函数有两个重载。第一个重载接受两个数字并返回一个数字,第二个重载接受两个字符串并返回一个字符串。
add
函数的类型,就是包含两个调用签名的Add
类型。
使用ReturnType
来获取Add
的返回值类型,得到是最后一个重载的返回值类型,因此上面例子中的type Fn12 = Fn1 & Fn2
,其返回值就是string
。
说了这么多,最终,我们要利用的,就是函数类型的交叉类型,其返回值是最后一个函数的返回值这个特性。
函数参数的infer类型推导
在之前的文章中,我详细介绍过infer类型推导,但是,有一个小点我没有细讲,那就是——函数类型的联合类型,用infer推导它的参数类型, 结果是参数的交叉类型。
如果不太理解,我来举例说明:
type F1 = (a: Date) => void
type F2 = (a: object) => void
type F = F1 | F2
type p = F extends (a: infer R) => void ? R : never // Date & object
这个例子中,F
是两个函数的联合类型,两个函数具有不同的参数类型,一个是object
,一个是Date
,我们使用infer
对F的参数进行推导,结果是二者的交叉类型Date & object
,这和同infer多占位问题非常相似。
至于为什么会这样,这涉及到TS的协变和逆变的区别,如果你不理解或没听说过这两个词,建议你记住结论就行了:infer类型推导,函数参数的位置是交叉类型,其他位置都是联合类型。
利用infer推导的这个特性,我们可以将联合类型转换为交叉类型。
UnionToTuple的实现
有了上述知识的铺垫,我们可以一步一步来实现这道题了。
先写一个简答的测试用例,联合类型A:
type A = object | Date
- 第一步,将普通的联合类型转为函数的联合类型:
type UnionToFn<T> = T extends infer R ? (fn: () => R) => void : never
// 测试
type Fn = UnionToFn<A> // ((fn: () => object) => void) | ((fn: () => Date) => void)
为了转为交叉类型做准备,需要将函数放在函数参数的位置。
联合类型的每一项,位于参数函数的返回值位置。
- 第二步,利用infer推导的特性,通过函数的联合类型获取参数的交叉类型:
type FnToIntersection<T> = [T] extends [(a: infer R) => void] ? R : never
// 测试
type Intersection = FnToIntersection<Fn> // (() => object) & (() => Date)
为了避免泛型的联合类型分发,这里需要使用中括号[]
,如果我们合并第一步和第二步,就可以省略中括号:
type UnionToIntersection<T> = (T extends infer R ? (fn: () => R) => void : never) extends (a: infer S) => void ? S : never
- 第三步,利用函数交叉类型的特性,获取最后一个重载的返回值:
type IntersectionLast<T> = T extends () => infer R ? R : never
// 测试
type IntersectionLast<Intersection> // Date
- 第四步,将前三步整合为
UnionLast
:
type UnionToFn<T> = T extends infer R ? (fn: () => R) => void : never
type FnToIntersection<T> = [T] extends [(a: infer R) => void] ? R : never
type IntersectionLast<T> = T extends () => infer R ? R : never
type UnionLast<T> = IntersectionLast<FnToIntersection<UnionToFn<T>>>
// 测试
type A = object | Date
type Last = UnionLast<A> // Date
或者:
type UnionToIntersection<T> = (T extends infer R ? (fn: () => R) => void : never) extends (a: infer S) => void ? S : never
type IntersectionLast<T> = T extends () => infer R ? R : never
type UnionLast<T> = IntersectionLast<UnionToIntersection<T>>
// 测试
type A = object | Date
type Last = UnionLast<A> // Date
- 最后,逐项提取联合类型的最后一项,放入元组中:
type UnionToTuple<T, R extends Array<unknown> = [], L = UnionLast<T>> = [T] extends [never] ? R : UnionToTuple<Exclude<T, L>, [...R, L]>
完整答案如下:
type UnionToIntersection<T> = (T extends infer R ? (fn: () => R) => void : never) extends (a: infer S) => void ? S : never
type IntersectionLast<T> = T extends () => infer R ? R : never
type UnionLast<T> = IntersectionLast<UnionToIntersection<T>>
type UnionToTuple<T, R extends Array<unknown> = [], L = UnionLast<T>> = [T] extends [never] ? R : UnionToTuple<Exclude<T, L>, [...R, L]>