对此的简短回答是,目前在 TypeScript 中无法对数字 literal types 执行任意数学计算。 microsoft/TypeScript#26382 有一个(相当长期的)开放功能请求要求这个。您可能想去那里给它一个 ?,因为它的状态是“等待更多反馈”,如果您认为它很有吸引力,您可能想在那里留下评论,详细说明您的用例。但这可能不会有太大的不同,所以现在最好只是假设类型级别的数学不会很快发生。
您可以通过操纵tuple types 来说服编译器执行某种数学运算,例如[any, any, any]。元组类型具有 length 属性,它们是数字文字,您可以使用 variadic tuple types 和 recursive 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 太小了,所以我们给它添加一个元素,然后再试一次。这会将少量数字切成两半:如果N 是10,那么T 变为[],然后是[0],然后是[0,0],然后是[0,0,0],然后是[0,0,0,0],然后是[0,0,0,0,0]满足原始检查,所以你得到5。
N extends any... 部分对N 中的联合进行distributive 操作,因此输入的联合成为输出中的联合...所以DivideByTwo<10 | 20> 是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。它构建长度为32、16、8、4的元组、2 和 1。然后它连接长度 16 和 4 以获得长度 20 之一。现在它将20除以4,通过构建长度为8、4、2和1的元组。然后它连接长度为4 和1 的长度为5 的长度之一。结果是5。
但是糟糕,丑陋,可怕。您不想在生产中做任何事情。
如果我是你,我会重新审视为什么你希望类型系统为你做这件事,而不是你自己做。而且,如果您仍然有充分的理由,那么在 microsoft/TypeScript#26382 上进行游说可能比使用元组跳过障碍更有效。
Playground link to code