0

Suppose I have this type:

type NS = {
    readonly A: 4;
    readonly B: 4;
    readonly Op: 2;
    readonly Cin: 1;
}

Then how can I statically know, with a type, that the sum of these ints is 11? I.e.,

type S = SumUp<NS> // how to implement SumUp so that S is 11?

I am using ts-arithmetic, which provides a handy Add<N, M> helper type, but I can't seem to find a way to recursively loop over all keys of NS and accumulate the results.

Jean-Philippe Pellet
  • 59,296
  • 21
  • 173
  • 234
  • If the keys are static, and there aren't that many, you could use `Add>>`. Otherwise, I'm pretty sure you would need to [tuplify](https://stackoverflow.com/questions/55127004/how-to-transform-union-type-to-tuple-type) a union. – kelsny Mar 10 '23 at 15:17
  • You could do it like [this](https://tsplay.dev/NlDqxN) without the need to tuplify a union but it's incredibly slow, generates a "Type instantiation is excessively deep and possibly infinite" error, and gets even worse if you have more than 4 keys. – kelsny Mar 10 '23 at 15:36
  • Using the already mentioned [tuplify](https://stackoverflow.com/a/55128956/438273) utility, does something like [this](https://tsplay.dev/mq9Xkm) work for you? If so, I can write it up as an answer. If not, what am I missing? – jsejcksn Mar 10 '23 at 15:41
  • Using tuplify union would look something like [this](https://tsplay.dev/Wo8paw), but again, there are some risks as jcalz outlined in his answer. – kelsny Mar 10 '23 at 15:41
  • Wow, great. Writeups as answers would be great, yes! – Jean-Philippe Pellet Mar 10 '23 at 15:55
  • @vr. Would you like to provide an answer using the technique in our comments? – jsejcksn Mar 11 '23 at 01:28

1 Answers1

2

If you don't have that many keys, you could always hardcode the adds,

type Summed = Add<NS["A"], Add<NS["B"], Add<NS["Op"], NS["Cin"]>>>;

otherwise, you may have to convert a union to a tuple, from this question. Here's one such implementation using an accumulator (it's faster because it is tail call optimized):

type _SumUp<T extends Record<string, number>, Keys, Result extends number = 0> =
    Keys extends [infer First extends keyof T, ...infer Rest]
        ? _SumUp<T, Rest, Add<Result, T[First]>>
        : Result;
type SumUp<T extends Record<string, number>> = _SumUp<T, TuplifyUnion<keyof T>>;

Internally, we "loop over" each of the keys and add the value to the result. Then the SumUp type is really just a wrapper around this internal helper, giving it T and its keys as a tuple.

If this doesn't fancy you because of the use of TuplifyUnion (and all its problems as described in the linked question), you could still do it:

//@ts-ignore Type instantiation is excessively deep and possibly infinite.(2589)
type SumUp<T extends Record<string, number>, K extends keyof T = keyof T> = [keyof T] extends [never] ? 0 : K extends K ? Add<T[K], SumUp<Omit<T, K>>> : never;

However, TS will generate an error, which I have not found a way around yet. So I have just resorted to ignoring it since it works for the inputs we give. This method is also a lot slower for TS to compute the result of (it is noticeably slower with a large delay).

Playground (hardcoded)

Playground (using TuplifyUnion)

Playground (no TuplifyUnion)

kelsny
  • 23,009
  • 3
  • 19
  • 48
  • Very cool! At least two linked problems with `TuplifyUnion` don't apply there: we don't rely on the ordering as adding in any order will work, and unexpected collapsing or expanding will not occur with a union of string literals. I don't know yet if in practice this will be too hard on the compiler. I will ever only have 3-5 members to loop on. – Jean-Philippe Pellet Mar 11 '23 at 19:52