打字稿:映射联合类型时的类型级数学

时间:2021-07-05 13:59:25

标签: typescript

是否可以使用类型级数学来映射打字稿中的联合类型以生成一个新联合,该联合是第一个联合类型的函数?

例如,我想使用现有的联合类型:

type foo = 768 | 1024 | 1280;

为了产生这个联合(每个选项除以 16):

type bar = 48 | 64 | 80;

工会成员的数量是灵活的,也就是说,它也可以是:
600 | 768 | 1024 | 1280

1 个答案:

答案 0 :(得分:1)

对此的简短回答是,目前在 TypeScript 中无法对数值 literal types 执行任意数学计算。 microsoft/TypeScript#26382 有一个(相当长的)开放功能请求要求这样做。您可能想去那里给它一个 ?,并且由于它的状态是“等待更多反馈”,如果您认为它引人注目,您可能想在那里发表评论,详细说明您的用例。但这可能不会有太大的不同,所以现在最好只是假设类型级数学不会很快发生。

您可以通过像 [any, any, any] 一样操作 tuple types 来说服编译器执行某种数学运算。元组类型具有 length 属性,它们是数字文字,您可以使用 variadic tuple typesrecursive conditional types 来更改元组长度。


这意味着您将仅限于对非负整数执行操作(您不能有长度为 3.14159-2 的元组),并且由于类型递归的限制约为大约 25 个深度级别,很难处理大量数据。直观的实现往往只适用于小于 25 或 50 左右的数字。有一些不太直观的实现可以处理数以千计或数以万计的数字(请参阅相关 GitHub 问题上的 this comment),但即使这些实现也涉及实际构建这些长度的元组,因此可能会使编译器陷入困境。即使你非常聪明,你也可能会写出一些复杂而脆弱的东西。边缘情况无处不在

您的原始代码示例将小数除以 2,可以使用这样的递归来完成:

type DivideByTwo<N extends number, T extends 0[] = []> = N extends any ?
  0 extends [...T, ...T, 0][N] ? T['length'] : DivideByTwo<N, [0, ...T]> : never;

type X = DivideByTwo<6 | 10 | 30>; // type X = 3 | 5 | 15

这是通过取一个数字 N 和一个以空 (T) 开头的元组 [] 来工作的。如果 T 的两个副本连接在一起后跟单个元素在索引 N 处有一个元素,那么 T 至少是 N 的两倍,我们只返回T 的长度。否则,T 太小,所以我们向它添加一个元素并重试。这将小数一分为二:如果 N10,则 T 变为 [],然后是 [0],然后是 [0,0],然后是 {{ 1}},然后是 [0,0,0],然后是满足原始​​检查的 [0,0,0,0],所以你得到 [0,0,0,0,0]

5 部分对 N extends any... 中的联合进行操作 distributive,因此输入的联合变成输出中的联合......所以 NDivideByTwo<10 | 20> (按某种顺序)。


但是随后您将示例更改为将显着大于 50 的数字除以 16。为了开始这样做,我必须开始使用重复加倍元组的技巧(这意味着递归深度限制最终看起来像是对数字对数的限制,而不是数字本身)。试图对除数(分子,大数)和除数(分母,这里是 16)都这样做对我来说什至不值得尝试。这是一个硬编码的东西,似乎适用于除以 16:

5 | 10

你喜欢吗?是的,我也没有。即使详细解释它是如何工作的,我也无能为力。我会说它的工作原理是除以四两次(当我直接尝试 16 时,编译器陷入困境并且无法在合理的时间内完成),并且它通过建立重复加倍长度的元组来除以四,停止当它有一个太大时,然后将它们连接在一起形成一个合适的长度。

草图:假设我们应该将 type Quadruple<T extends any[]> = [...T, ...T, ...T, ...T] type Explode<N extends number, R extends never[][]> = Quadruple<R[0]>[N] extends never ? R : Explode<N, [[...R[0], ...R[0]], ...R]>; type BinaryBuilder<N extends number, R extends never[][], B extends never[]> = Quadruple<[...B, never]>[N] extends never ? B : Quadruple<[...R[0], ...B]>[N] extends never ? BinaryBuilder<N, R extends [R[0], ...infer U] ? U extends never[][] ? U : never : never, B> : BinaryBuilder<N, R extends [R[0], ...infer U] ? U extends never[][] ? U : never : never, [...R[0], ...B]>; type CutTupleInFour<N extends number> = number extends N ? any[] : N extends number ? Explode<N, [[never]]> extends infer U ? U extends never[][] ? BinaryBuilder<N, U, []> : never : never : never; type DivideByFour<N extends number> = CutTupleInFour<N>['length'] type DivideBySixteen<N extends number> = DivideByFour<DivideByFour<N>> type Foo = 768 | 1024 | 1280; type Bar = DivideBySixteen<Foo> // type Bar = 64 | 48 | 80 除以 16。编译器首先将 80 除以 4。它构建长度为 80, 32 的元组、16842。然后连接长度为 116 的长度以获得长度为 4 的长度。现在它通过构建长度为 202084 的元组将 2 除以 4。然后连接长度为 14 的长度以获得长度为 1 的长度。结果是5

但是糟糕,丑陋,糟糕。您不想在生产中做任何事情。


如果我是你,我会重新审视为什么你希望类型系统为你做这件事而不是你自己做。如果您仍然有充分的理由,那么在 microsoft/TypeScript#26382 上为它进行游说可能比使用元组跳过障碍更有效。

Playground link to code