[TypeScript奇技淫巧] union to tuple

在 github 上看到一段有意思的代码,从一个接口类型获得了它的属性的tuple类型。原版的代码有一些不足,我改进了一下

让我们来先想一下,如果想要把 A | B 变成 [A, B] 应该怎么做。很自然的我们会想到用 conditional type 去 inference 出 AB 然后构造出 [A, B]

type UnionToTuple<Union> = Union extends infer A | infer B ? [A, B] : never;
type tuple = UnionToTuple<'a' | 'b'>; // [{}, {}]

结果并非如我们所想,我们得到了 [{}, {}],而且它实际上是 [{}, {}] | [{}, {}] 的结果,这是因为 conditional type 设计为 distributive 的(可以看这里)。

既然如此,那么我们就不能把 union 直接用于 conditional type 了,那么开头提到的 github 上的代码是怎么做到的呢?我们先不管 union,简单来说,作者的思路就是利用了函数类型的 intersection 等价于 接口的 call signature 重载。也就是说:

type f1 = ((x: number) => void) & ((x: string) => void);

type f2 = {
    (x: number): void;
    (x: string): void;
}

type same = f1 extends f2 ? true : false; // true

于是我们可以用 conditional type 来 infer 出 intersection 里的类型:

type FnIntersectionToTuple<T> = T extends {
    (x: infer A): void;
    (x: infer B): void;
} ? [A, B] : never;
type intersection = ((x: number) => void) & ((x: string) => void);
type tuple = FnIntersectionToTuple<intersection>; // [number, string]

所以只要我们把 union 转换成这样的 intersection,就能分离出 union 里的类型。这里我们就又可以用到 zzj聚聚这篇 里的技巧把 union 转成 intersection:

type UnionToIntersection<U> =
    (U extends any ? (k: U) => void : never) extends
    ((k: infer I) => void) ? I : never

这个代码先是利用 conditional type 把 U (U1 | U2 | ...) distribute 成 ((k: U1) => void) | ((k: U2) => void) | ...,然后 extends ((k: infer I) => void) 由于函数类型的参数位是逆变的,会 infer 出 U1 & U2 & ...

但是我们需要获得的是 ((x: U1) => void) & ((x: U2) => void) & ... 这样的 intersection,不过只需要稍微改一下 k 的类型即可:

// union to intersection of functions
type UnionToIoF<U> =
    (U extends any ? (k: (x: U) => void) => void : never) extends
    ((k: infer I) => void) ? I : never
type intersection = UnionToIoF<'a' | 'b'>; // ((x: "a") => void) & ((x: "b") => void)

至此,我们就有了把 union 转成 intersection,再提取出其中的元素的方法了。但是能提取多少个元素取决于我们写几个 infer,比如一次提取8个元素类型到tuple:

// union to intersection of functions
type UnionToIoF<U> =
    (U extends any ? (k: (x: U) => void) => void : never) extends
    ((k: infer I) => void) ? I : never

// intersection of functions to tuple
type IoFToTuple<IoF> = IoF extends {
    (a: infer A): void;
    (b: infer B): void;
    (c: infer C): void;
    (d: infer D): void;
    (e: infer E): void;
    (f: infer F): void;
    (g: infer G): void;
    (h: infer H): void;
} ? [A, B, C, D, E, F, G, H] : never;

// 一次从 union 中提取8个元素到tuple
type UnionToTuple8<U> = IoFToTuple<UnionToIoF<U>>;

不过我们可以循环地提取,并从 union 中排除取得的类型,直到 union 为空:

// 连接8个元素的tuple到另一个tuple
type Concat8<A extends any[], B extends any[]> =
    ((a: A[0], b: A[1], c: A[2], d: A[3], e: A[4], f: A[5], g: A[6], h: A[7], ...r: B) => void) extends
    (...r: infer R) => void ? R : never;

// 递归地提取 union 中的元素到 tuple
type UnionToTupleRecursively<Union, Result extends any[]> = {
    1: Result;
    0: UnionToTupleRecursively_<Union, UnionToTuple8<Union>, Result>;
    // 0: UnionToTupleRecursively<Exclude<Union, UnionToTuple8<Union>[number]>, Concat8<UnionToTuple8<Union>, Result>>
}[[Union] extends [never] ? 1 : 0];

// 或者直接使用上面注释掉的代码
type UnionToTupleRecursively_<Union, Tuple8 extends any[], Result extends any[]> =
    UnionToTupleRecursively<Exclude<Union, Tuple8[number]>, Concat8<Tuple8, Result>>;

type EraseEmptyTypes<T> = { [P in keyof T]: {} extends T[P] ? never : T[P] };

// 最终我们得到了 UnionToTuple
type UnionToTuple<U> = EraseEmptyTypes<UnionToTupleRecursively<U, []>>;

由于 intersection 和 infer 的数量不匹配时会推出空接口类型,这里简单地写了个 EraseEmptyTypes 将空接口替换成 never,实际应该有更好的办法,等想到简单的办法再完善。现在我们可以先试一下效果:

type UnionMoreThan100 = keyof [
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0
];
type keys = UnionToTuple<UnionMoreThan100>;
// [never, never, never, never, never, "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20", "21", "22", ... 107 more ..., number]

type works_for_any_unions = UnionToTuple<{ x: number } | (() => number)>;
// [never, never, never, never, never, never, { x: number; }, () => number]

实现这个意义是什么呢?以前我们有 mapped type,可以把接口类型 map 到另一种类型,现在我们有了 UnionToTuple,利用递归类型(就像我在 UnionToTupleRecursively 里那样做的),我们可以迭代类型的属性了。

至于用法的例子,我暂时没想到特别好玩的,如果大家有什么想法可以玩玩看了。不过个人建议,不要花太多时间在这些上面……

Update:

试了一下其实不用一次提取多个元素,简化了一下代码如下:

// union to intersection of functions
type UnionToIoF<U> =
    (U extends any ? (k: (x: U) => void) => void : never) extends
    ((k: infer I) => void) ? I : never

// return last element from Union
type UnionPop<U> = UnionToIoF<U> extends { (a: infer A): void; } ? A : never;

// prepend an element to a tuple.
type Prepend<U, T extends any[]> =
    ((a: U, ...r: T) => void) extends (...r: infer R) => void ? R : never;

type UnionToTupleRecursively<Union, Result extends any[]> = {
    1: Result;
    0: UnionToTupleRecursively_<Union, UnionPop<Union>, Result>;
    // 0: UnionToTupleRecursively<Exclude<Union, UnionPop<Union>>, Prepend<UnionPop<Union>, Result>>
}[[Union] extends [never] ? 1 : 0];

type UnionToTupleRecursively_<Union, Element, Result extends any[]> =
    UnionToTupleRecursively<Exclude<Union, Element>, Prepend<Element, Result>>;

type UnionToTuple<U> = UnionToTupleRecursively<U, []>;

Peace!

编辑于 2019-03-13