TS类型体操 困难题——Union to Tuple 联合类型转元组 最详细的解题过程解析

402 阅读5分钟

TS类型体操(一) 基础知识

TS类型体操(二) TS内置工具类1

TS类型体操(三) TS内置工具类2

TS类型体操(四) 操场搭建以及热身运动

前言

今天我们来做一下一道困难级别的题: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

这里的Fn1Fn2是两个函数类型,返回值类型分别是numberstringFn12是二者的交叉类型。

请问: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]>