Existential Types in Typescript Through Continuations
(this post was originally from a comment I made on /r/typescript)
My favorite “advanced” thing about Typescript is that while it does not currently support existential types, you can actually encode existential types through continuations.
For example, say you have the following interface and known implementations:
interface Property<T> {
pget: () => T;
pset: (value: T) => void;
}
class NumberProperty implements Property<number> {
constructor(private value: number) {
}
pget() { return this.value }
pset(value: number) { this.value = value }
}
class DateProperty implements Property<Date> {
constructor(private value: Date) {
}
pget() { return this.value }
pset(value: Date) { this.value = value }
}
Now you want to maintain a list of Properties. You don’t care what types each individual Property is, just that they are valid properties. How do you type this?
let properties: Property<???>[];
You could always put in any
or unknown
, but we know that leads to problems down the line–unknown
would require casting to use, but we don’t know what type to cast to. And any
would let us do bad things like having properties where the getters and setters have different types.
Ideally we’d be able to tell Typescript “for each individual Property in my list, the Property is over a generic type T
, and even though I don’t know what T
is, it exists”, which is essentially what existential types are. There are GH issues suggesting syntax like Property<*>
for this, but who knows when they’ll make progress.
For now, you can actually get this working in a roundabout way using continuations:
type PropertyCont = <R>(cont: <T>(prop: Property<T>) => R) => R;
function makePropCont<T>(property: Property<T>): PropertyCont {
return <R>(cont: <T>(prop: Property<T>) => R) => cont(property);
}
let properties: PropertyCont[];
This abuses the cool inference Typescript has for functions to allow us to encode a generic type without ever having to say what that type is–existential types!
Now, we can use this list of Properties as intended without any casts or any
:
properties.push(
makePropCont(new NumberProperty(0)),
makePropCont(new DateProperty(new Date)),
// ERR won't pass as the generic type isn't internally consistent
// which is correct behavior
makePropCont({
pget: () => 44,
pset: (badValue: string) => { }
})
);
properties.forEach(cont => cont(prop => {
// val is of type `T`, which while we haven't defined anywhere
// TS is able to infer
const val = prop.pget();
prop.pset(val);
}));