Write an algorithm in normal TypeScript and you reach for a loop without thinking. Write the same algorithm in the type system and the loop is gone. There is no for, no while, no mutable counter to increment. A type cannot change; it can only be something. The only way to repeat work is to define a type in terms of itself and let the compiler unfold it.

Every experiment in this archive is built on that single move: the Wordle engine, the bracket validator, and Two Sum. Once you can see it, the type-level code stops looking like a puzzle and starts looking like ordinary recursion.

The shape of a type-level loop

A recursive type has two halves. A base case stops, and a recursive case does a little work and hands the rest back to itself. Conditional types (T extends U ? X : Y) are the only branching available, so they carry both.

Reversing a tuple is the smallest example that shows the whole shape:

type Reverse<T extends unknown[]> =
  T extends [infer Head, ...infer Tail]
    ? [...Reverse<Tail>, Head]   // peel one off the front, recurse on the rest
    : [];                        // empty input: nothing left to reverse

type R = Reverse<[1, 2, 3]>;     // [3, 2, 1]

[infer Head, ...infer Tail] is the type-level equivalent of const [head, ...tail] = arr. The conditional asks whether there is still an element to peel off. If yes, we strip Head, reverse what remains, and stick Head on the end. If no, we hit [] and stop. There is no index variable and no mutation. The type keeps referring to a smaller version of itself until the input runs dry.

The same peeling works on strings, using a template literal instead of a tuple pattern:

type Length<S extends string, Acc extends unknown[] = []> =
  S extends `${infer _Head}${infer Tail}`
    ? Length<Tail, [...Acc, unknown]>   // one more character counted
    : Acc['length'];

type L = Length<'wordle'>;              // 6

That second type parameter, Acc, is the part worth slowing down on.

The accumulator pattern

Reverse above builds its result on the way back out of the recursion. Each call waits for Reverse<Tail> to finish before it can wrap Head around the answer. That works, but the compiler holds a stack of half-finished types, each one paused waiting on the next.

The accumulator pattern flips this. You carry the answer forward as an extra parameter and finish it before the final call returns:

type ReverseAcc<T extends unknown[], Acc extends unknown[] = []> =
  T extends [infer Head, ...infer Tail]
    ? ReverseAcc<Tail, [Head, ...Acc]>   // answer-so-far travels with the call
    : Acc;                               // base case already holds the result

type R = ReverseAcc<[1, 2, 3]>;          // [3, 2, 1]

Notice what changed. In ReverseAcc, the recursive call is the entire body of the true branch, and nothing happens to its result afterward. The work ([Head, ...Acc]) is done before the call, packed into the argument. That property is called a tail call, and it is the difference between a type that scales and one that explodes.

This is why Valid Parentheses threads a tuple P through every step as a stack, and why Two Sum carries a record of values it has already seen. They never look back at a partial result. The state rides along in the accumulator, and the answer is complete the moment the base case fires.

Why tail calls matter: TS 4.5

Before TypeScript 4.5, deep recursion of any kind hit a wall around 45-50 levels with the infamous error:

Type instantiation is excessively deep and possibly infinite. ts(2589)

That ceiling exists because each paused conditional type costs the compiler a frame, and it refuses to nest them without bound. A non-tail-recursive type like Reverse wraps Head around the result after recursing. It burns one frame per element and tips over on inputs longer than a few dozen items.

TypeScript 4.5 added tail-recursion elimination for conditional types. When the recursive call sits in tail position, with its result returned directly and no further work wrapped around it, the compiler evaluates it iteratively instead of stacking frames. A tail-recursive type can run roughly a thousand iterations before hitting its limit, an order of magnitude past where the naive version dies.

The practical rule:

  • Result built on the way out ([...Recurse<Tail>, Head]) → not a tail call → fails early.
  • Result carried in an accumulator (Recurse<Tail, [Head, ...Acc]>) → tail call → runs deep.

If a type-level function needs to process a long string or a big tuple, rewriting it into accumulator form is usually the fix that takes it from ts(2589) to a clean answer. The Wordle engine relies on this. Scoring five letters across six guesses is dozens of recursive steps, and it only stays under the limit because each pass returns its recursive call directly rather than post-processing it.

Reading recursion in the wild

Once the pattern is in your eye, every experiment decomposes the same way. Find the base case (the branch with no self-reference), find the peel ([H, ...T] or `${H}${T}`), and check whether the recursive call is returned bare or wrapped. Bare means it scales. Wrapped means it is living on borrowed depth.

That is the whole vocabulary. The type system gives you one loop, itself, and an accumulator to carry state through it. Everything else in the archive is built from those two pieces.

Next: how that string-peeling template literal turns the type checker into a parser.