TypeScript's Dark Arts: Building a Type-Level Calculator
TypeScript is its own programming language :3

Just a guy who loves to write code and watch anime.
Search for a command to run...
TypeScript is its own programming language :3

Just a guy who loves to write code and watch anime.
No comments yet. Be the first to comment.
Qualities that make outstanding builders.

Bipedal vs quadrupedal: how many legs This is about the number of legs the creature walks on. Bipedal. Two legs. Humans, ostriches, T-rex, kangaroos, most fantasy humanoids. Quadrupedal. Four legs. Do

Intro You hear "lerp" and "smoothstep" everywhere in game dev. They sound like math jargon. They're not. Both are small tools that do the same job: smoothly move from one value to another. The problem

What is Kinematics Kinematics is the math field. It's the study of how things move without worrying about forces (which would be dynamics). FK and IK are the two branches: forward kinematics and inver

Intro Textures are usually the biggest cost in a 3D scene. Memory, bandwidth, and load time all get eaten by them. Resizing them is the obvious lever. There's more. This post is about the less obvious

Have you ever wondered if we could do math without runtime? ๐ฑ
Yeah... we're gonna build a little calculator in TypeScript. It's gonna be fun hehe, a bit of a brain teaser. ๐
Weโll do addition, subtraction, division and multiplication.
First, let's start with something surprising: TypeScript's type system is Turing complete. This means we can do calculations using just types.
type IsTwo<T> = T extends 2 ? true : false;
type Result = IsTwo<2>; // true
Cute, but boring. Let's get wild.

In type-land, we can represent numbers as tuples of a specific length.
Why? Because we can measure them!
type BuildTuple<
Length extends number,
Result extends Array<unknown> = []
> = Result["length"] extends Length
? Result
: BuildTuple<Length, [...Result, unknown]>;
// BuildTuple<3> = [unknown, unknown, unknown]
// Basically: Give me an array of 'Length' unknown elements
// BuildTuple<5>["length"] = 5 ๐
"But that's just an array!" I hear you say. Yes, but it's an array we can count. And if we can count, we can do math! ๐
Here's where it gets spicy. Want to subtract 3 from 5? Easy! Just:
Build a tuple of length 5
Try to match it against a pattern that has a tuple of length 3
Count what's left (by inferring the rest of the tuple)
type Subtract<
Value extends number,
SubtractAmount extends number
> = BuildTuple<Value> extends [...BuildTuple<SubtractAmount>, ...infer Rest]
? Rest["length"]
: never;
type ThreeMinusTwo = Subtract<3, 2>; // 1
When we do Subtract<5, 3>, here's the galaxy-brain moment of what happens:
// 1. BuildTuple<5> creates [unknown, unknown, unknown, unknown, unknown]
// 2. BuildTuple<3> creates [unknown, unknown, unknown]
// 3. Pattern matching:
// [unknown, unknown, unknown, unknown, unknown] extends
// [...[unknown, unknown, unknown], ...infer Rest]
// 4. Rest captures [unknown, unknown]
// 5. Rest['length'] = 2 ๐
Take both numbers, build tuples of the appropriate lengths, and spread them together.
type Add<A extends number, B extends number> = Length
[...BuildTuple<A>, ...BuildTuple<B>]
>
type AddTwoAndThree = Add<2, 3>; // [unknown, unknown, unknown, unknown, unknown]
AddTwoAndThree["length"]; // 5
type Multiply<
A extends number,
B extends number,
Result extends Array<unknown> = []
> = B extends 0
? Length<Result>
: Multiply<A, Subtract<B, 1>, [...Result, ...BuildTuple<A>]>
Multiplication is a bit tricky to understand at first. Let's break it down:
type MultiplyTwoByThree = Multiply<2, 3>; // 6
It's MultiplyTwoByThree is 6 here and we don't need to use ["length"] because we're returning Length<Result>.
The idea is we keep spreading in the tuple of A until we've done it B times. If B extends 0, it means B is 0 therefore we return the length of the result.
Let's go over two iterations of MultiplyTwoByThree. First iteration:
type Multiply<
2,
3,
[]
// 3 extends 0 is false!
> = 3 extends 0
? Length<[]>
// Build a tuple of 2 elements
// Spread it into the result along with the empty array (initial results)
// `B` is now 2 with Subtract<3, 1>
: Multiply<2, Subtract<3, 1>, [...[], ...BuildTuple<2>]>
Second iteration:
type Multiply<
2,
2,
[unknown, unknown]
// 2 extends 0 is false!
> = 2 extends 0
? Length<[unknown, unknown]>
// Build a tuple of 2 elements
// Spread it into the result along with [unknown, unknown]
// `B` is now 1 with Subtract<2, 1>
: Multiply<2, Subtract<2, 1>, [...[unknown, unknown], ...BuildTuple<2>]>
Division is a bit different. Multiplication was repeated addition. Division is repeated subtraction.
type Divide<
A extends number,
B extends number,
Count extends Array<unknown> = []
> = A extends 0
? Length<Count>
: // This is never if `A` is less than `B`
Subtract<A, B> extends never
? Length<Count>
: Divide<Subtract<A, B>, B, [...Count, unknown]>;
How it works is Count keeps increasing the number of times we've subtracted B from A until A is less than B.
Imagine we have 6 / 2. What you do is you decrement 6 by 2, and every time you do that, you increment a counter. We know it's three, but let's go over it together:
6 - 2 = 4
4 - 2 = 2
2 - 2 = 0
In the fourth iteration, A is now 0. We return the length of the count, which is 3.
type DivideSixByTwo = Divide<6, 2>; // 3
Let's go over this one together. Two iterations like we did before:
type Divide<
6,
2,
[]
> = 6 extends 0
? Length<[]>
: Subtract<6, 2> extends never
? Length<[]>
: Divide<Subtract<6, 2>, 2, [...[], unknown]>
Second iteration:
type Divide<
4,
2,
[unknown]
> = 4 extends 0
? Length<[unknown]>
: Subtract<4, 2> extends never
? Length<[unknown]>
: Divide<Subtract<4, 2>, 2, [...[unknown], unknown]>
Some utility types that can come in handy:
type Length<T extends Array<unknown>> = T["length"];
type First<T extends Array<unknown>> = T extends [infer First, ...unknown[]]
? First
: never;
type Last<T extends Array<unknown>> = T extends [...infer _, infer Last]
? Last
: never;
// You can do any number!
type Zero = 0;
type One = 1;
type Two = 2;
type Three = 3;
type Four = 4;
type Five = 5;
type Six = 6;
type Seven = 7;
type Eight = 8;
No. Most of the times, if dealing with complicated TypeScript types, it's abstracted away from you by e.g. a library.
However, when building larger systems, you may build your own utilities and abstractions, so knowing TypeScript at a deeper level can be helpful, but not necessary!
The real magic isn't in what these types do. It's in realizing that TypeScript's type system is like its own programming language. We're not just defining types. We're creating programs that work during compilation.
Our BuildTuple type works like a recursive function. Our Subtract type uses pattern matching. We're doing functional programming, but with types!