Description
Sometimes we need to capture a union of types that may be a mixture of primitives, functions, objects, etc. but we want to leave it parameterised (just as we already can with interfaces).
Example
A simple non-recursive example:
type Source<T> = T | (() => T);
Here, a source can either be a plain value or a nullary function that obtains a value.
We can provide uniform access to such sources:
function unwrap<T>(p: Source<T>) {
return (typeof p === "function") ? p() : p;
}
And then we can specify model interfaces where we we leave open the nature of the source but we tie down the value types:
interface Person {
name: Source<string>;
age: Source<number>;
}
e.g. name
is a constant, but age
depends on when you ask:
var p: Person = {
name: "John Lennon",
age: () => ageFromDOB(1940, 10, 9),
}
But we can treat them identically in consuming code:
var n = unwrap(p.name), a = unwrap(p.age);
NB. The above is already possible with union types alone, but the interface Person
has to repeat the pattern:
interface Person {
name: string | (() => string);
age: number | (() => number);
}
Not so bad for a simple example, but the pattern for a value source might evolve to get more complex and then you have a lot of fiddly updating to do because you "Did Repeat Yourself".
Recursion
The more flexible recursive version:
type Source<T> = T | (() => Source<T>);
A source can either be a plain value or a nullary function that obtains a source (which may be a plain value terminating recursion, or a nullary function that... and so on).
We can again provide uniform access to such sources, either with runtime recursion (risky until tail-call optimisation is widespread):
function unwrap<T>(p: Source<T>) {
return (typeof p === "function") ? unwrap(p()) : p;
}
Or with a loop:
function unwrap<T>(p: Source<T>) {
for (;;) {
if (typeof p !== "function") {
return p;
}
p = p();
}
}