简体   繁体   English

Typescript:映射联合类型时的类型级数学

[英]Typescript: type-level math when maping over a union type

Is it possible to use type-level math to map over a union type in typescript to produce a new union that is a function of the first one?是否可以通过 typescript 中的联合类型对 map 使用类型级数学来生成一个新的联合,即第一个联合的 function?

Eg, I'd like to use an existing union type:例如,我想使用现有的联合类型:

type foo = 768 | 1024 | 1280;

in order to produce this union (each option divided by 16):为了产生这个并集(每个选项除以 16):

type bar = 48 | 64 | 80;

Where the number of members in the union is flexible, ie, it could just as well be:如果工会中的成员数量是灵活的,也可以是:
600 | 768 | 1024 | 1280

The short answer to this is that it is not currently possible in TypeScript to perform arbitrary mathematical computations on numeric literal types .对此的简短回答是,目前在 TypeScript 中无法对数字文字类型执行任意数学计算。 There is a (fairly longstanding) open feature request at microsoft/TypeScript#26382 asking for this.microsoft/TypeScript#26382上有一个(相当长期的)开放功能请求,要求这样做。 You might want to go there and give it a, and since its status is "awaiting more feedback", you might want to leave a comment there detailing your use case if you think it's compelling.您可能想在那里 go 并给它一个,并且由于它的状态是“等待更多反馈”,如果您认为它令人信服,您可能想在此处留下详细说明您的用例的评论。 But it probably won't make much of a difference, so for now it's best to just proceed on the assumption that type-level math won't happen anytime soon.但这可能不会有太大的不同,所以现在最好只是假设类型级别的数学不会很快发生。

You can convince the compiler to perform some sorts of mathematical operations by manipulating tuple types like [any, any, any] .您可以通过操纵[any, any, any]之类的元组类型来说服编译器执行某种数学运算。 Tuple types have length properties which are numeric literals, and you can use variadic tuple types and recursive conditional types to change tuple lengths.元组类型具有数字文字的length属性,您可以使用可变元组类型递归条件类型来更改元组长度。


That means you will be limited to doing operations on non-negative whole numbers (you can't have a tuple of length 3.14159 or -2 ), and since type recursion has a limit of about ~25 levels of depth 1000 levels of depth ( UPDATE : TS4.5 has increased this to about ~1000 levels for tail-recursive conditional types so this is a bit better now, but it still is pretty small when it comes to numbers), it is hard to get things working with large numbers.这意味着您将仅限于对非负整数进行操作(您不能拥有长度为3.14159-2的元组),并且由于类型递归的限制约为 25 级深度 1000 级深度(更新:对于尾递归条件类型,TS4.5 已将其提高到约 1000 个级别,因此现在情况有所好转,但在数字方面仍然很小),很难处理大数字. The intuitive implementations tend to only work with numbers less than 1000 or so.直观的实现往往只适用于小于 1000 左右的数字。 There are some less intuitive implementations that can work with numbers in the thousands or tens of thousands (see this comment on a related GitHub issue) but even these involve actually building tuples of those lengths, and can thus bog down the compiler.有一些不太直观的实现可以处理数千或数万的数字(请参阅有关 GitHub 相关问题的评论),但即使这些实现也涉及实际构建这些长度的元组,因此可能会使编译器陷入困境。 Even if you are incredibly clever, you will probably write something complicated and fragile.即使你非常聪明,你也可能会写出复杂而脆弱的东西。 Edge cases are everywhere .边缘案例无处不在

Your original code example was dividing small numbers by two, which can be done using recursion like this:您的原始代码示例是将小数除以二,这可以使用递归来完成,如下所示:

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

This works by taking a number N and a tuple T which starts off empty ( [] ).这通过取一个数字N和一个以空 ( [] ) 开始的元组T来工作。 If two copies of T concatenated together followed by a single element has an element at index N , then T is at least as big as twice N , and we just return the length of T .如果T的两个副本连接在一起,后跟单个元素在索引N处有一个元素,那么T至少是N的两倍,我们只返回T的长度。 Otherwise, T is too small, so we add an element to it and try again.否则, T太小,所以我们添加一个元素,然后再试一次。 This chops small numbers in half: if N is 10 , then T becomes [] , then [0] , then [0,0] , then [0,0,0] , then [0,0,0,0] , then [0,0,0,0,0] which satisfies the original check, and so you get 5 .这将小数字切成两半:如果N10 ,则T变为[] ,然后[0] ,然后[0,0] ,然后[0,0,0] ,然后[0,0,0,0] ,然后[0,0,0,0,0]满足原始检查,所以你得到5

The N extends any... part makes the operation distributive over unions in N , so unions of inputs become unions in outputs... so DivideByTwo<10 | 20> N extends any...部分使操作在N中的联合上分配,因此输入的联合成为输出中的联合...所以DivideByTwo<10 | 20> DivideByTwo<10 | 20> is 5 | 10 DivideByTwo<10 | 20>5 | 10 5 | 10 (in some order). 5 | 10 (按某种顺序)。


But then you changed your example to be dividing numbers significantly larger by sixteen.但是后来你改变了你的例子,将数字除以十六。 To even begin doing this I'd have to start using the trick of repeated doubling of tuples (which means the recursion depth limit ends up looking like a limit on the logarithm of the number and not the number itself).为了开始这样做,我必须开始使用重复加倍元组的技巧(这意味着递归深度限制最终看起来像是对数字的对数而不是数字本身的限制)。 And trying to do that for both the dividend (the numerator, the big number) and the divisor (the denominator, the 16 here) is not worth it to me to even try.并且尝试对除数(分子,大数)和除数(分母,这里的 16)都这样做对我来说是不值得尝试的。 Here's a hardcoded thing that seems to work for dividing by sixteen:这是一个硬编码的东西,似乎可以除以十六:

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

You like that?你喜欢? Yeah, me neither.是的,我也没有。 Even explaining how it works in detail is too much for me to do.即使详细解释它是如何工作的,我也无能为力。 I'll say that it works by dividing by four twice (when I tried sixteen directly the compiler bogged down and wouldn't complete in a reasonable time), and that it divides by four by building up tuples of repeated-doubling lengths, stops when it has one that's too big, and then joins them together to form one of the right length.我会说它通过两次除以四来工作(当我直接尝试十六时,编译器陷入困境并且无法在合理的时间内完成),并且它通过建立重复加倍长度的元组来除以四,停止当它有一个太大时,然后将它们连接在一起形成一个合适的长度。

A sketch: let's say we're supposed to divide 80 by 16. The compiler first divides 80 by 4. It builds tuples of lengths 32 , 16 , 8 , 4 , 2 , and 1 .草图:假设我们应该将80除以 16。编译1首先将80 8 4它构建长度为3216的元2 It then concatenates the ones of lengths 16 and 4 to get one of length 20 .然后它连接长度为164的长度为20的长度。 Now it divides 20 by 4, by building tuples of lengths 8 , 4 , 2 , and 1 .现在它通过构建长度为8421的元组来将20除以 4。 It then concatenates the ones of lengths 4 and 1 to get one of length 5 .然后它连接长度41以获得长度5之一。 And the result is 5 .结果是5

But yuck, ugly, awful.但是,糟糕,丑陋,可怕。 Nothing you'd want to do in production.你不想在生产中做任何事情。


If I were you, I'd re-examine why you want the type system to do this for you instead of doing it yourself.如果我是你,我会重新审视为什么你希望类型系统为你做这件事,而不是你自己做。 And if you still have a good reason, it might be more effective lobbying for it at microsoft/TypeScript#26382 than it is to jump through hoops with tuples.而且,如果您仍然有充分的理由,那么在 microsoft/TypeScript#26382 上进行游说可能比使用元组跳过障碍更有效。

Playground link to code Playground 代码链接

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM