简体   繁体   中英

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?

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):

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 . There is a (fairly longstanding) open feature request at microsoft/TypeScript#26382 asking for this. 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. 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] . Tuple types have length properties which are numeric literals, and you can use variadic tuple types and recursive conditional types to change tuple lengths.


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. The intuitive implementations tend to only work with numbers less than 1000 or so. 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. 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 ( [] ). 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 . Otherwise, T is too small, so we add an element to it and try again. 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 .

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> DivideByTwo<10 | 20> is 5 | 10 5 | 10 (in some order).


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. 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 . It then concatenates the ones of lengths 16 and 4 to get one of length 20 . Now it divides 20 by 4, by building tuples of lengths 8 , 4 , 2 , and 1 . It then concatenates the ones of lengths 4 and 1 to get one of length 5 . And the result is 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.

Playground link to code

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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