Generate the FizzBuzz sequence as a tuple of string literals, without loops or a modulo operator. Tuple counters and a tail-recursive accumulator do the work.
FizzBuzz again, except this time there is no runtime and no modulo operator. You solve it entirely in types.
The FizzBuzz problem is a classic test given in coding interviews. The task is simple:
Print integers 1 to N, except:
For example, for N = 20, the output should be:
1, 2, Fizz, 4, Buzz, Fizz, 7, 8, Fizz, Buzz, 11, Fizz, 13, 14, FizzBuzz, 16, 17, Fizz, 19, Buzz
In the challenge below, we will generate this as an array of string literals. For large values of N your types need to evaluate efficiently, which means using tail-call optimisation for the recursion. Along the way you'll meet the two workhorses of type-level arithmetic: tuples as counters, and accumulators that keep recursion in tail position.
The FizzBuzz problem is a classic test given in coding interviews. The task is simple:
Print integers 1 to N, except:
For example, for N = 20, the output should be:
1, 2, Fizz, 4, Buzz, Fizz, 7, 8, Fizz, Buzz, 11, Fizz, 13, 14, FizzBuzz, 16, 17, Fizz, 19, Buzz
In the challenge below, we will want to generate this as an array of string literals.
For large values of N, you will need to ensure that any types generated do so efficiently (e.g. by correctly using the tail-call optimisation for recursion).
View on GitHub: https://tsch.js.org/14080
Change the following code to make the test cases pass (no type check errors).
👋 Lifetime-License is leaving on August 10, 2026
Get it now for $29
One-time payment. Lifetime access to all pro challenges.
The full solution first, then the breakdown:
type Word<
N extends number,
Three extends number,
Five extends number,
> = Three extends 3
? Five extends 5
? 'FizzBuzz'
: 'Fizz'
: Five extends 5
? 'Buzz'
: `${N}`
type Build<
N extends number,
Acc extends string[] = [],
Counter extends 1[] = [1],
Three extends 1[] = [1],
Five extends 1[] = [1],
> = Acc['length'] extends N
? Acc
: Build<
N,
[...Acc, Word<Counter['length'], Three['length'], Five['length']>],
[...Counter, 1],
Three['length'] extends 3 ? [1] : [...Three, 1],
Five['length'] extends 5 ? [1] : [...Five, 1]
>
type FizzBuzz<N extends number> = Build<N>There's no arithmetic in the type system, no + and no %. Everything here is built from one primitive: the length of a tuple type is a numeric literal type.
If you can't add, you can still append. Growing a tuple by one element increments its length:
type Two = [1, 1]['length'] // 2
type Three = [...[1, 1], 1]['length'] // 3Build maintains three such counters:
Counter: the current value being emitted; Counter['length'] is the number itself.Three: a cycle counter that runs 1, 2, 3, then resets to 1.Five: the same, cycling 1 through 5.The cycle counters replace the modulo operator: instead of asking "is n % 3 === 0?", we reset a small tuple every time it reaches length 3. Whenever Three['length'] is exactly 3, the current number is divisible by 3. No division needed:
[object Object]This trick is worth memorizing: divisibility checks become counting in cycles.
Word maps the two cycle positions to the output string:
type Word<N extends number, Three extends number, Five extends number> =
Three extends 3
? Five extends 5 ? 'FizzBuzz' : 'Fizz'
: Five extends 5 ? 'Buzz' : `${N}`The interesting case is the last one: `${N}` embeds a numeric literal type in a template literal, converting it to a string literal. For N = 7 that evaluates to '7', which is why FizzBuzz<5> yields ['1', '2', 'Fizz', '4', 'Buzz'] with the numbers as strings.
A quick intermediate check of one iteration: at value 15, both counters have just hit their maximum, so Word<15, 3, 5> evaluates the Three extends 3 branch, then Five extends 5, and produces 'FizzBuzz'.
The naive way to build a tuple recursively is [Head, ...Recurse<...>]: recurse first, prepend after. TypeScript has to keep every pending prepend on its internal stack, and around 50 levels deep it gives up with "Type instantiation is excessively deep". That would sink the FizzBuzz<100> test.
Build avoids this with an accumulator. The growing result Acc is passed down as a parameter, and the recursive call is the entire result of the false branch, so nothing is left to do after it returns. TypeScript recognizes this shape (tail recursion) and reuses the same evaluation slot instead of stacking, raising the limit from ~50 nested instantiations to ~1000 iterations. That's the "tail-call optimisation" the problem statement asks for.
The stopping condition is checked against the accumulator itself:
[object Object]Once we've emitted N words, Acc['length'] is exactly N and the finished tuple is returned. Because the check happens before each append, FizzBuzz<1> runs exactly one iteration and returns ['1'].
FizzBuzz<N> = Build<N> exists so the public type has a single parameter, while Build carries its four pieces of internal state with default values (Acc = [], all counters starting at [1], since the first value emitted is 1). Hiding accumulator parameters behind a thin wrapper is standard practice for recursive type utilities. Callers should never see, or be able to tamper with, your internal state.
FizzBuzz<1>: the smallest case; the accumulator pattern must not emit anything past N.FizzBuzz<20>: includes 15, the first 'FizzBuzz', confirming both cycles reset correctly and line up again.FizzBuzz<100>: 100 iterations, far beyond the non-tail-recursive depth limit. If you wrote [Word<...>, ...Build<...>] instead of an accumulator, this is the test that would fail.Tuple counters plus a tail-positioned accumulator is the standard template for any "generate a sequence" type: ranges, repeated strings, padded numbers, and beyond.
This challenge is originally from here.