Skip to content
webtype.orgTwelve concepts · 10/12

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.

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