My original problem is this.
I made the following type but it does not work due to the circular references error and I don't know how to solve it:
type Increment<T extends number, Tuple extends any[] = [any]> =
T extends 0 ? 1 :
T extends 1 ? 2 :
T extends TupleUnshift<any, Tuple> ?
TupleUnshift<any, TupleUnshift<any, Tuple>>['length'] :
Increment<T, TupleUnshift<any, TupleUnshift<any, Tuple>>>
In the end it should work like:
type five = 5
type six = Increment<five> // 6
P.S. TupleUnshift
is from here.
I think in the other question and in comments I said that you can try to do this recursively but that the compiler, even if you can make it happy with the definition, will give up at some relatively shallow depth. Let's look at it here:
EDIT: You asked for an explanation, so here it is:
First, the definition of
Cons<H, T>
uses tuples in rest/spread positions and conditional types to take a head typeH
and a tuple tail typeT
and return a new tuple in which the head has been prepended to the tail. (The name "cons" comes from list manipulation in Lisp and later in Haskell.) SoCons<"a", ["b","c"]>
evaluates to["a","b","c"]
.As background, TypeScript generally tries to stop you from doing circular types. You can sneak around the back door by using some conditional types to defer some execution, like this:
This should either evaluate to
BaseCase
orRecursiveThing<...T...>
, but the conditional type inside the index access gets deferred, and so the compiler doesn't realize that it will end up being a circular reference. Unfortunately this has the very bad side effect of causing the compiler to bottom out on infinite types and often getting bogged down when you start using these things in practice. For example, you can defineTupleOfIncrementedLength<>
like this:and it works, even for
N
up to 40 or 50 or so. But if you stick this in a library and start using it you might find that your compile time becomes very high and even causes compiler crashes. It's not consistent, so I can't easily generate an example here, but it's bitten me enough for me to avoid it. It's possible this problem will eventually be fixed. But for now, I'd follow the advice of @ahejlsberg (lead architect for TypeScript):Enter @strax, who discovered that since
interface
declarations are not evaluated eagerly the way thattype
declarations are (sincetype
s are just aliases, the compiler tries to evaluate them away), if you can extend aninterface
in the right way, in conjunction with the conditional type trick above, the compiler should not get bogged down. My experiments confirm this, but I'm still not convinced... there could be a counterexample that only shows up when you try to compose these things.Anyway, the above
TupleOfIncrementedLength
type withReduction
andReduce
works much the same way as the "naïve" version from before, except that it is aninterface
. I really can't pick it apart without writing ten pages and I don't feel like doing it, sorry. The actual output is aReduction
whose1
property has the tuple we care about.After that,
Increment
is defined in terms ofTupleOfIncrementedLength
by getting the1
property and extracting itslength
(I don't use plain indexed access because the compiler can't deduce thatTupleOfIncrementedLength<N>[1]
is an array type. Luckily conditional type inference saves us).I don't know that it's too useful for me to walk through exactly how this works.Suffice it to say that it keeps increasing the length of a tuple until its length is one greater than theN
parameter, and then returns the length of that tuple. And it does work, for smallN
:But, on my compiler at least (version 3.3.0-dev.20190117), this happens: