More fun with recursive types in Typescript

Typescript allows a type to reference another type defined further down in the same file, which can lead to rather interesting constructs.

The vanilla circular reference fails:

type Foo = Bar; // error: type alias Foo circularly references itself
type Bar = Foo; // error: type alias Bar circularly references itself

However, the following is legal, and surprisingly deep:

type Foo = Bar;
type Bar = Foo[];

What it defines is a recursive list of empty lists:

  • The empty list [] is in the type.
  • A list of any values already in the type is in the type.

This is pretty close to Von Neumann construction of natural numbers:

// combine two Foo lists
function union(left: Foo, right: Foo) : Foo {
  return [...left, ...right];
}

// von Neumann definition of "next number"
function succ(x: Foo) : Foo {
  return union(x, [x]);
}

const zero: Foo = [];
const one: Foo = succ(zero);        // [[]]
const two: Foo = succ(one);         // [[],[[]]]
const three: Foo = succ(two);       // [[],[[]],[[],[[]] ] ]
console.log(JSON.stringify(three)); // prints [[],[[]],[[],[[]]]]

We can go ahead and implement addition and even multiplication of Foos (but it’s going to be very slow):

function isZero(x: Foo) {
  return x.length === 0;
}

function pred(x: Foo) : Foo {
  if (isZero(x)) {
    throw new Error("Zero has no predecessor");
  }
  return x.slice(0, -1); // all elements except the last one
}

function add(a: Foo, b: Foo): Foo {
  if (isZero(b)) {
    return a;
  } else {
    return succ(add(a, pred(b)));
  }
}

function multiply(a: Foo, b: Foo): Foo {
  if (isZero(b)) {
    return zero;
  } else {
    return add(a, multiply(a, pred(b)));
  }
}

console.log( JSON.stringify(add(one, two)));        // [[],[[]],[[],[[]]]]
console.log( JSON.stringify(multiply(two, two)));   // [[],[[]],[[],[[]]],[[],[[]],[[],[[]]]]]

Recursive types are definitely fun.

Leave a Reply

Your email address will not be published. Required fields are marked *