Skip to content
webtype.orgwebtype.orgTwelve concepts · 11/12

    ↑↓ move · ⏎ open · esc close

    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.

    Exercise

    Add an element to the front of a tuple.

    Try it

    2
    • Unshift<[2, 3], 1>
      [1, 2, 3]
    • Unshift<[], 'a'>
      ['a']

    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.

    Practiced in