【问题标题】:Typescript: type-level math when maping over a union type打字稿:映射联合类型时的类型级数学
【发布时间】:2021-07-07 04:31:13
【问题描述】:

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

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

type foo = 768 | 1024 | 1280;

为了产生这个并集(每个选项除以 16):

type bar = 48 | 64 | 80;

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

【问题讨论】:

  • 你的例子有问题;使用 distributive conditional types 映射联合类型很简单。但是不直接支持将数字文字类型乘以 2。它需要像ms/TS#26382 这样的东西来实现,或者同时需要一个复杂/脆弱的解决方法。也许将示例更改为您知道如何对单个元素进行操作的示例?除非你真的在问类型级别的数学。
  • 我猜你的例子是除以二,而不是相乘。请参阅this code 了解我将为特定示例做些什么;如果这看起来太疯狂并且您并不真正关心数学,则应考虑更改示例(例如,将 2 | 4 | 8 变为 {x: 2} | {x: 4} | {x: 8},即 much less crazy
  • @jcalz 谢谢。我真的在问类型级数学。 The code you linked to 似乎是我所需要的,但我不确定我是否理解那里发生了什么,或者如果我想除以另一个数字而不是 2,我将如何改变它。
  • 当我有机会时,我很乐意详细说明答案,但与此同时,您可能想要编辑问题以询问数学(这是棘手/不可能的部分)而不是映射over unions(这是最简单的部分)

标签: typescript


【解决方案1】:

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

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


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

您的原始代码示例是将小数除以二,这可以使用递归来完成,如下所示:

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],然后是[0,0,0],然后是[0,0,0,0],然后是[0,0,0,0,0]满足原始检查,所以你得到5

N extends any... 部分对N 中的联合进行distributive 操作,因此输入的联合成为输出中的联合...所以DivideByTwo&lt;10 | 20&gt;5 | 10(按某种顺序)。


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

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

草图:假设我们应该将80除以16。编译器首先将80除以4。它构建长度为321684的元组、21。然后它连接长度 164 以获得长度 20 之一。现在它将20除以4,通过构建长度为8421的元组。然后它连接长度为41 的长度为5 的长度之一。结果是5

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


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

Playground link to code

【讨论】:

  • 哇,感谢您的深入回答和解释。
猜你喜欢
  • 2021-01-26
  • 2019-12-11
  • 2020-06-07
  • 2019-11-19
  • 2022-08-17
  • 2022-12-07
  • 1970-01-01
  • 2017-05-18
  • 1970-01-01
相关资源
最近更新 更多