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

    ↑↓ move · ⏎ open · esc close

    Concept 10 of 12

    Recursion

    There are no loops at the type level, so recursion is the only way to process something of unknown length. The shape is always the same: split off a piece, handle it, recurse on the rest, and stop when there is nothing left.

    Head and tail

    type Reverse<T extends readonly unknown[]> =
      T extends readonly [infer H, ...infer R] ? [...Reverse<R>, H] : []
    
    // The order you REBUILD in is the whole difference:
    //   [H, ...Reverse<R>]   copies
    //   [...Reverse<R>, H]   reverses

    The false branch is the exit and must return the identity of whatever you are building: `[]` for tuples, `''` for strings, `{}` for objects. Return the payload there instead and you get an off-by-one that only shows at the end.

    Accumulators build forwards

    type Unique<T extends readonly unknown[], Acc extends unknown[] = []> =
      T extends readonly [infer H, ...infer R]
        ? Includes<Acc, H> extends true ? Unique<R, Acc> : Unique<R, [...Acc, H]>
        : Acc

    A defaulted parameter carries state the caller never passes. Beyond correctness — this is what lets `Unique` keep the *first* occurrence rather than the last — a tail-recursive shape also uses less of the compiler’s stack than building the answer on the way out.

    There is a depth limit

    // Around 1000 levels, then:
    // "Type instantiation is excessively deep and possibly infinite."

    TypeScript stops at roughly a thousand instantiations. Tail recursion raises the ceiling considerably, but the limit is real and is why nobody ships a type-level calculator that handles large numbers.

    The common wrong answer

    // Intent: flatten completely, to any depth.
    type Flatten<T extends readonly unknown[]> =
      T extends readonly [infer H, ...infer R]
        ? H extends readonly unknown[]
          ? [...H, ...Flatten<R>]     // <- only unwraps ONE layer
          : [H, ...Flatten<R>]
        : []
    
    Flatten<[[[1]]]>   // [[1]] — stopped one level early

    Spreading `H` opens one layer; recursing into it opens all of them. Recursing in one direction walks a list, in two directions walks a tree — and "it only went one level deep" is almost always a missing second recursive call.

    Exercise

    Unwrap a promise however many layers deep it is.

    Try it

    2
    • Unwrap<Promise<string>>
      string
    • Unwrap<Promise<Promise<number>>>
      number

    Takeaway

    Split, handle, recurse, stop. The base case returns the identity, and the direction you reassemble in is what distinguishes a copy from a reversal, a filter or a map.

    Practiced in