ts联合类型怎么转元组?

type Union = 'a' | 'b' | 'c';
// 转
type Tuple = ['a', 'b', 'c'];
阅读 6.4k
2 个回答
答案
type UnionToIntersection<U> = (U extends any ? (a: (k: U) => void) => void : never) extends (a: infer I) => void ? I : never;
type UnionLast<U> = UnionToIntersection<U> extends (a: infer I) => void ? I : never;
type UnionToTuple<U> = [U] extends [never] ? [] : [...UnionToTuple<Exclude<U, UnionLast<U>>>, UnionLast<U>];

传送门:Code in ts playground


为了解决这个题,去查了不少资料。一直想尝试用一种新的看似更简单的方法来解决这个问题,最后发现不太可能,所以还是这个网上能找到的答案,顶多只是写法略有不同。

本来想利用 U extends any 会遍历 U 中的原始类型的特点(TypeScript: Distributive conditional types,通过递归组合成 [...],结果失败。它最后只能得出联合,不能单个遍历。

仔细想一下,在 TypeScript 能遍历的好像只有 { ... } 的属性,但这个问题似乎又没办法封装成对象接口类型来处理。想来想去,好像只能通过上面 UnionLast 所使用的方法,使用对函数参数的逆变推导来得到联合中的单一类型。OK 这里又涉及到了协变和逆变的知识点,而要搞明白协变和协变,还得清楚类型的父/子级关系,也就是 extends 关系。具体的可以搜,这里总结两点:

父子级关系

如果有 A、B 两个类型,说“A 是 B”为真,而 “B 是 A”不为真,则 A 是 B 的子类型;如果都为真,A 和 B 是同一类型,或者等价类型;如果返过来,则 B 是 A 的子类型。

举例来说。“狗是动物”为真,所以“狗”是“动物”的子类型;“动物是狗”不为真,所以“动物”不是“狗”的子类型。在 TypeScript 中
type A = { a: number, b: string } 包含了 type B = { a: number } 的属性,所以可以说 A 是 B,也就是说 A 是 B 的子类型;"hello"string,所以字符字量类型是 string 的子类型 ……

协变和逆变

父类型可以接受子类型的赋值。所以如果有函数类型 (arg: A) => A,它的结果可以赋值给 A 的父类型,但参数只能是 A 的子类型。因为结果是输出,参数是输入,所以可以理解为输出到父类型为协变,而输入需要子类型为逆变。如果都以函数作为“参照物”来看,自身返回类型 A 对应于外部类型 BaseOfA,“A 是 BaseOfA”成立,符合我们前面的父子级观点,称之为“协”;而自身参数类型对应于外部类型 SubOfA,“A 是 SubOfA”不成立,得反过来才成立,称之为“逆”。

在这个基础之上,可以理解 AA | B | C 的子类型。才能够理解 UnionLast 可以通过函数参数的逆变,从联合类型中推断出最后一个符合的类型 A,相当于从一个数组中找到第一个符合条件的元素,而条件是“非 never”,也就是找第 1 个元素。然后 UnionToTuple 干的就是不断找第一个元素,推入 Tuple,再把这个元素从原类型集合中去掉,递归行进……

接下来就需要理解 UnionToIntersection 的作用了,看这篇:Ts高手篇:22个示例深入讲解Ts最晦涩难懂的高级类型工具 - 掘金 (juejin.cn)。这里是转相交类型的方法,加上前面提到的需要通过逆变来推导类型,而逆变又只能通过函数参数来实现,所以答案中算的是若干函数的相交类型。


最后还是有一点遗憾 —— 不能掌握结果 Tuple 的顺序,而且结果 Tuple 的顺序似乎并不稳定。注意下面动画中的 Tuple3,一开始是 ["a", "b", undefined],改了 Union 之后它变成了 [undefiend, "a", "b"] (注意:Tuple3 跟 Union 本身一点关系都没有)

captured-1.gif

参考

如果只是不想重复声明可以尝试转变下思路,可以使用数值来获取两种类型:

export const tuple = <T extends string[]>(...args: T) => args;

const arr = tuple('a', 'b', 'c');
type Tuple = typeof arr;
// type Tuple = ["a", "b", "c"]
type Union = typeof arr[number]
// type Union = "a" | "b" | "c"
撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进