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 Foo
s (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.