TypeScript's Dark Arts: Building a Type-Level Calculator

TypeScript's Dark Arts: Building a Type-Level Calculator

TypeScript is its own programming language :3

ยท

5 min read

Introduction

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.

The Basics: Types as Values

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.

Naruto Smile by Narutoria on DeviantArt

Building Numbers From Scratch

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! ๐Ÿ˜

Subtraction

Here's where it gets spicy. Want to subtract 3 from 5? Easy! Just:

  1. Build a tuple of length 5

  2. Try to match it against a pattern that has a tuple of length 3

  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 ๐Ÿ˜

Addition

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

Multiplication

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

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:

  1. 6 - 2 = 4

  2. 4 - 2 = 2

  3. 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]>

Utility Types

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;

Will I ever need this?

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

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!

ย