hrtyy.dev

Type-Level Arithmetic in TypeScript, type Year2024 = Add<2000, 24>

Introduction to Type-Level Addition in TypeScript using Type Manipulation

Normally, if you want to do addition in TypeScript, you can do it just using "+" operator.

index.ts
1const a = 1 + 2; // 3

You can run this code using some tools like ts-node, and you can get the result of 3 in runtime.

But, what if you want to do arithmetic in compile time?

1type Add<A extends number, B extends number> = A + B;

The above code is not valid. It will cause a compile error. A and B is just a type, not a value, "+" operator is not defined for them.

Tuple Type in TypeScript

In fact, you can do addition in compile time using tuple type. But why?

Tuple type is kind of an array type, but it has fixed length and position. Fixed position means compiler knows which type is in which position in build time.

1type StringNumberPair = [string, number];

For example, the above StringNumberPair type is a tuple which has two elements, first element is string, second element is number. And, you can get the length of tuple type using length property, this is an interesting feature.

1// StringNumberPair["length"] is same as literal type 2
2const pairLength1: StringNumberPair["length"] = 2;
3const pairLength2: 2 = 2;

Use this feature for type-level addition.

1. type-level value

As I mentioned, "+" operator is not defined for number type, so normal number type is not suitable for type-level addition. I will define a new type.

1type Value<NUM extends number, LOOP extends number[] = []> = LOOP["length"] extends NUM ? LOOP : Value<NUM, [...LOOP, 0]>

Value type represents number but its format is tuple type. For example,

  • Value<3> is [0, 0, 0]
  • Value<5> is [0, 0, 0, 0, 0]

In this context, the elements of tuple type is not important, the important thing is the length of tuple type.

2. type-level addition

Tuple type can be spread using ... operator, so you can get the type-level addition type Add as the following.

1type Add<A extends number, B extends number> = [...Value<A>, ...Value<B>]["length"];

This code works correctly.

1type Ex1 = Add<3, 5> // 8
2type Ex2 = Add<100, 100> // 200
3type Ex3 = Add<123, 456> // 579

OK, try 2000 + 24.

1type Ex4 = Add<2000, 24>

This code does not work with the following error message.

Type instantiation is excessively deep and possibly infinite.

This error is thrown by a TypeScript compiler. According to checker.ts,

checker.ts:L18656
1// We loop here for an immediately nested conditional type in the false position, effectively treating
2// types of the form 'A extends B ? X : C extends D ? Y : E extends F ? Z : ...' as a single construct for
3// purposes of resolution. We also loop here when resolution of a conditional type ends in resolution of
4// another (or, through recursion, possibly the same) conditional type. In the potentially tail-recursive
5// cases we increment the tail recursion counter and stop after 1000 iterations.
6while (true) {
7 if (tailCount === 1000) {
8 error(currentNode, Diagnostics.Type_instantiation_is_excessively_deep_and_possibly_infinite);
9 return errorType;
10 }
11
12...
checker.ts:L19911
1if (instantiationDepth === 100 || instantiationCount >= 5000000) {
2 // We have reached 100 recursive type instantiations, or 5M type instantiations caused by the same statement
3 // or expression. There is a very high likelyhood we're dealing with a combination of infinite generic types
4 // that perpetually generate new type identities, so we stop the recursion here by yielding the error type.
5 tracing?.instant(tracing.Phase.CheckTypes, "instantiateType_DepthLimit", { typeId: type.id, instantiationDepth, instantiationCount });
6 error(currentNode, Diagnostics.Type_instantiation_is_excessively_deep_and_possibly_infinite);
7 return errorType;
8}

There are 2 positions can throw this error. In this case, the former code is executed. I created Value type using recursion and conditional type. Each recursion increments the length of tuple type by 1. To create Value<2000>, the length of tuple type is 2000, the recursion is executed 2000 times.

Simply,

1type V1 = Value<999> // OK
2type V2 = Value<1000> // NG. Type instantiation is excessively deep and possibly infinite.

There is limitation.

Type-Level Adder

We found there are some limitations for type manipulation. To solve this problem, I created a library called typed-circuit

In this library, Half Adder and Full Adder are implemented. There are some restriction, but 16bit addition is available.

1type BitRepr_6 = [HalfByte0, HalfByte0, HalfByte0, [0, 1, 1, 0]];
2type BitRepr_11 = [HalfByte0, HalfByte0, HalfByte0, [1, 0, 1, 1]];
3
4const BitRepr_17: Adder16<BitRepr_6, BitRepr_11> = [
5 [0, 0, 0, 0],
6 [0, 0, 0, 0],
7 [0, 0, 0, 1],
8 [0, 0, 0, 1],
9];

Or, simply

1const Value2024: Add<2000, 24> = 2024; // OK
2const Value4721: Add<1829, 2892> = 4721; // OK
3const Value8001: Add<4000, 4001> = 8001; // OK

If you are interested, please check here

Thank you for reading!