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.
