4/16/2026
Only Types Programs
When Your Types ARE the Algorithm
A while back I watched this video of DOOM running in the TypeScript type system, and me and my friend Yoav from my unit got curious about how far you can push TS types. We obviously didn’t get anywhere near what dimitropoulos is doing, but it was a fun rabbit hole — and it reframed how I think about types entirely.
Types as a program
Usually when you write TypeScript, types are just a development-time aid: they help you understand what a variable holds, give you autocomplete, and stop you from doing dumb things. Once you compile, the types disappear. The output is plain JavaScript, because JavaScript has no type system.
So from the runtime’s perspective, types are not part of the program — they’re just scaffolding that gets stripped away. What’s surprising is that the scaffolding itself is Turing-complete. You can write real programs inside the type system, and the compiler runs them for you at type-check time. The “result” is the type it infers. No JS ever runs.
To do that, we need a few building blocks.
Arithmetic (without built-in arithmetic)
The examples below are real little programs: they need counting, adding, subtracting. TypeScript types do not give you that out of the box — you cannot write type X = 10 - 4, and there is no single “add these two type-level numbers” operator. So we’re gonna use a few tricks to get around that. The next snippets show add and subtract done that way.
One small thing that matters: for a string type, length is just number — TypeScript does not keep the real character count in the type. For a tuple (a fixed-length array type), it does keep the exact length as a literal:
type Str = "string"
type Arr = [1, 2, 3, 4]
type StrLen = Str["length"]
// type StrLen = number
type ArrLen = Arr["length"]
// type ArrLen = 4So these tricks use tuples, not string length.
Addition
type NumToArr<N extends number, Arr extends never[] = []> =
Arr["length"] extends N ? Arr : NumToArr<N, [...Arr, never]>
type Plus<A extends number, B extends number> =
[...NumToArr<A>, ...NumToArr<B>]["length"] & number
type Ten = Plus<5, 5>
// type Ten = 10NumToArr<N> grows a tuple one slot at a time until it has length N. Plus<A, B> makes two of those, merges them with spread, and takes the length. The & number bit helps TypeScript keep the answer as a specific number type like 10, not plain number.
Subtraction
type Minus<A extends number, B extends number> =
NumToArr<A> extends [...NumToArr<B>, ...infer Rest] ? Rest["length"] : never;Here we build a tuple of length A and try to match it as “first B items, then the rest.” If that works, the length of “the rest” is A - B. If B is bigger than A, it does not match and you get never.
Multiplication is adding over and over; division is subtracting over and over. Same idea.
With that in hand, let’s actually build something.
Fibonacci
type NumToArr<N extends number, Arr extends never[] = []> =
Arr["length"] extends N ? Arr : NumToArr<N, [...Arr, never]>
type Plus<A extends number, B extends number> =
[...NumToArr<A>, ...NumToArr<B>]["length"] & number
type Minus<A extends number, B extends number> =
NumToArr<A> extends [...NumToArr<B>, ...infer Rest] ? Rest["length"] : never;
type OneFibonacci<Num extends number> =
Num extends 1 ? 1 :
Num extends 2 ? 1 :
Plus<OneFibonacci<Minus<Num, 1>>, OneFibonacci<Minus<Num, 2>>>
type Fibonacci<Num extends number, Arr extends any[] = []> =
Num extends Arr["length"] ? Arr :
Fibonacci<Num, [...Arr, OneFibonacci<Plus<Arr["length"], 1>>]>
type FibonacciA = Fibonacci<17>
// type FibonacciA = [1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597]Two pieces here:
OneFibonacci<N>computes theN-th Fibonacci number. The base cases are1and2, both mapping to1. The recursive case is the textbook definition:fib(n) = fib(n-1) + fib(n-2), expressed with ourPlusandMinustypes.Fibonacci<N>builds the sequence up toNelements. It keeps an accumulator tupleArr, and on each recursive step appendsOneFibonacci<Arr.length + 1>. WhenArr’s length reachesN, we’re done.
So Fibonacci<17> doesn’t run any code at runtime — the compiler evaluates the recursion while type-checking and gives back a fully computed tuple type.
The wall you eventually hit
This is fun, but it’s not free. TypeScript limits recursion depth to keep the compiler from hanging on pathological types, and our definitions are deeply recursive (especially OneFibonacci, which branches into two recursive calls). Push it a bit further and the compiler gives up:
type FibonacciA = Fibonacci<19>
// type FibonacciA = [1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, number, number]The last two entries degrade to number — meaning “I know it’s a number, I just couldn’t figure out which one before I hit my recursion budget.” This is the compiler’s polite way of bailing out instead of crashing.
Still, the fact that it works at all for 17 elements feels wild to me.
FizzBuzz
This one is inspired by Gal Hagever’s take and this video, though my implementation takes a slightly different route.
type NumToArr<N extends number, Arr extends never[] = []> =
Arr["length"] extends N ? Arr : NumToArr<N, [...Arr, never]>
type Plus<A extends number, B extends number> =
[...NumToArr<A>, ...NumToArr<B>]["length"] & number
type Minus<A extends number, B extends number> =
NumToArr<A> extends [...NumToArr<B>, ...infer Rest] ? Rest["length"] : never;
type IsDividedBy<Num extends number, Divider extends number> =
Num extends Divider ? true :
Minus<Num, Divider> extends never ? false :
IsDividedBy<Minus<Num, Divider>, Divider>
type OneFizzBuzz<Num extends number> =
IsDividedBy<Num, 15> extends true ? "FizzBuzz" :
IsDividedBy<Num, 3> extends true ? "Fizz" :
IsDividedBy<Num, 5> extends true ? "Buzz" :
Num;
type FizzBuzz<Num extends number, Arr extends any[] = []> =
Num extends Arr["length"] ? Arr :
FizzBuzz<Num, [...Arr, OneFizzBuzz<Plus<Arr["length"], 1>>]>
type FizzBuzzA = FizzBuzz<30>
// type FizzBuzzA = [1, 2, "Fizz", 4, "Buzz", "Fizz", 7, 8, "Fizz", "Buzz", 11, "Fizz", 13, 14, "FizzBuzz", 16, 17, "Fizz", 19, "Buzz", "Fizz", 22, 23, "Fizz", "Buzz", 26, "Fizz", 28, 29, "FizzBuzz"]The key trick is IsDividedBy. Without %, we check divisibility by subtracting Divider from Num until one of three things happens:
Num extends Divider— they’re equal, so it divides evenly. Returntrue.Minus<Num, Divider> extends never— we overshot (subtraction “failed” becauseB > A), so it doesn’t divide evenly. Returnfalse.- Otherwise — keep subtracting and recurse.
From there, OneFizzBuzz<Num> is just the usual ladder: check 15 first (to catch both), then 3, then 5, otherwise the number itself. Importantly, the branch order matters — if we checked 3 before 15, every multiple of 15 would be labeled "Fizz".
Finally, FizzBuzz<Num> is the same accumulator pattern as Fibonacci: grow a tuple one element at a time, feeding the current index into OneFizzBuzz until we reach Num entries.