Concept 11 of 12
Variadic tuples
Tuples are the type level’s only data structure with a length, which makes them its only counter. Every piece of type-level arithmetic you will ever see is counting elements in a tuple wearing a disguise.
Spreads concatenate
type A = [1, 2] type B = [3, 4] type Joined = [...A, ...B] // [1, 2, 3, 4] type Prefixed = [0, ...A] // [0, 1, 2] type Len = Joined['length'] // 4 — a numeric literal type
For a fixed-length tuple, `length` is a numeric literal rather than `number`. That single fact is what makes arithmetic possible: build a tuple of the right size, then read its length back out.
Addition is concatenation, subtraction is matching
type Build<N extends number, Acc extends unknown[] = []> = Acc['length'] extends N ? Acc : Build<N, [...Acc, unknown]> type Add<A extends number, B extends number> = [...Build<A>, ...Build<B>]['length'] type Sub<A extends number, B extends number> = Build<A> extends [...Build<B>, ...infer Rest] ? Rest['length'] : never
Building a tuple adds; taking one apart subtracts. The accumulator holds `unknown` because its contents are irrelevant — it is a tally of marks, not a collection of anything.
A rest element need not be last
type Last<T> = T extends readonly [...unknown[], infer L] ? L : never type Init<T> = T extends readonly [...infer I, unknown] ? I : never
A tuple pattern may contain exactly one rest element, and it is allowed at the front. That is how you reach the end of a tuple without counting your way there.
The common wrong answer
// Intent: subtract. type Sub<A extends number, B extends number> = [...Build<A>, ...Build<B>]['length'] // That is addition with a different name — concatenating // can only ever make the tuple longer.
Subtraction has to take a tuple apart, which means pattern matching rather than building. If your arithmetic type only ever spreads, it can only ever add.
Takeaway
Tuple length is the only number the type system can count with. Build to add, destructure to subtract, and expect the recursion limit around a thousand.