TS类型转换:union(联合)转tuple(元组)

需求

需求就是标题所示,需要有一个泛型类型能够把联合类型转化成一个相应顺序的元组类型

1
2
type SourceUnion = 'key1' | 'key2' | 'key3'
type TargetTuple = UnionToTuple<SourceUnion> // ['key1', 'key2', 'key3']

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
* 将联合类型转为对应的交叉函数类型
* @template U 联合类型
*/
type UnionToInterFunction<U> =
(U extends any ? (k: () => U) => void : never) extends
((k: infer I) => void) ? I : never

/**
* 获取联合类型中的最后一个类型
* @template U 联合类型
*/
type GetUnionLast<U> = UnionToInterFunction<U> extends { (): infer A; } ? A : never

/**
* 在元组类型中前置插入一个新的类型(元素);
* @template Tuple 元组类型
* @template E 新的类型
*/
type Prepend<Tuple extends any[], E> = [E, ...Tuple]

/**
* 联合类型转元组类型;
* @template Union 联合类型
* @template T 初始元组类型
* @template Last 传入联合类型中的最后一个类型(元素),自动生成,内部使用
*/
type UnionToTuple<Union, T extends any[] = [], Last = GetUnionLast<Union>> = {
0: T;
1: UnionToTuple<Exclude<Union, Last>, Prepend<T, Last>>
}[[Union] extends [never] ? 0 : 1]

思路

简单的来说就是使用递归算法,每次从联合类型(集合)中取出一个类型(元素),将该类型插入到已有的元组类型中,直至联合类型所有类型被取完;

img

难点

虽然思路说起来简单,但是由于TS类型系统目前很多语法没有被提供,因此无法像手写JS那样能够轻松遍历和递归之类的操作,因此会存在很多难点;

如何取出联合类型中的一个元素?

虽然联合类型从行为上来看可以视为集合,但是TS本身并没有给联合类型提供类似从集合中取值的操作;

1
2
type Union = 'a' | 'b' | 'c'
type E = Union[1] // Property '1' does not exist on type 'Union'

所以要从联合类型中取出某个位置的元素,只能另辟蹊径;

  • 重载的函数在使用infer进行推断时,重载的部分会取最后一个声明:

    1
    2
    3
    4
    5
    type FF = {
    (): 'b';
    (): 'a';
    } // 一个重载的函数类型
    type G = ReturnType<FF> // 'a'

    ReturnType是内置的泛型类型,会返回函数的返回类型

    1
    type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any
  • 将相同形状的函数类型进行交叉,等价于函数重载

    1
    2
    3
    4
    5
    6
    type FF = {
    (): 'b';
    (): 'a';
    }
    type B = (() => 'b') & (() => 'a') // 函数的交叉
    type G = FF extends B ? true : false // true

基于上述两个特性,就能够实现从联合类型中取出一个元素,只不过这个元素只能是最后一个:

  1. 将联合类型转为交叉函数类型(利用函数参数类型可以逆变的特性)

    1
    2
    3
    4
    5
    6
    7
    /**
    * 将联合类型转为对应的交叉函数类型
    * @template U 联合类型
    */
    type UnionToInterFunction<U> =
    (U extends any ? (k: () => U) => void : never) extends
    ((k: infer I) => void) ? I : never
  2. 获取交叉函数类型的返回类型

    1
    2
    3
    4
    5
    /**
    * 获取联合类型中的最后一个类型
    * @template U 联合类型
    */
    type GetUnionLast<U> = UnionToInterFunction<U> extends { (): infer A; } ? A : never

这样就可以做到获取联合类型中的最后一个元素了:

1
type G = GetUnionLast<'a' | 'b'> // 'b'

联合类型如何进行递归?

在TS类型系统中如果对(嵌套)对象类型进行递归,是可以直接调用自身的,如:

1
type DeepReadonly<T> = { readonly [P in keyof T]: DeepReadonly<T[P]> }

如果泛型本身是联合类型,想要通过直接在类型中调用自身就会报错:

1
2
3
4
5
// Type alias 'UnionToTuple' circularly references itself.
type UnionToTuple<Union, T extends any[] = [], Last = GetUnionLast<Union>> =
[Union] extends [never] ?
T :
UnionToTuple<Exclude<Union, Last>, Prepend<T, Last>>

上述递归方式TS会报错,不过好在有一个迂回的解决办法:就是将三元运算结果包装成一个对象类型,然后使用索引访问结果

1
2
3
4
5
6
7
8
9
10
/**
* 联合类型转元组类型;
* @template Union 联合类型
* @template T 初始元组类型
* @template Last 传入联合类型中的最后一个类型(元素),自动生成,内部使用
*/
type UnionToTuple<Union, T extends any[] = [], Last = GetUnionLast<Union>> = {
0: T; // true分支
1: UnionToTuple<Exclude<Union, Last>, Prepend<T, Last>> // false分支
}[[Union] extends [never] ? 0 : 1]

相关文档