There are some use cases for empty type: type that is not satisfied by any value. It behaves like an opposite of mixed: you can do anything with it, but can't assign any value to it. So, any becomes type any = mixed | empty.
It can be used:
switch when every option is exhausted:``` js
type T = 'Foo' | 'Bar'
switch (t) {
case 'Foo': return 1;
case 'Bar': return 2;
default: throw new Error(unknown type: ${t}); // here t should be empty
}
```
process.exit in node);``` js
let x: ?string = 'foo';
if (!x) {
console.error('x is not specified');
process.exit(-1);
}
doStuff(x); // type of x should be string, not ?string
```
I was thinking about this too recently and have one more use-case for empty. Let say we have an abstraction for async programming, but this abstraction also supports two kinds of results: success and failure. So it's basically async+Either. Let's call it Task. So we have a type Task<Success, Failure>. But sometimes we need only async functionality, i.e. we want to create a Task that may only succeed. empty would work great for this: Task<string, empty>.
Also let say that we have a function that can run 2 Tasks in parallel:
<S1, S2, F1, F2>(task1: Taks<S1, F1>, task2: Task<S2, F2>) => Task<[S1, S2], F1 | F2>
If we call such function with a one task that can't fail, and one that can, we get a task that also can fail:
const task1: Task<string, empty> = ...
const task2: Task<number, string> = ...
const result: Task<[string, number], string> = parallel(task1, task2)
I was using null & void as empty. This works as type that has no values. But it don't have a special feature that proper empty would have: empty | T = T. With this feature the parallel example would work, but with ad-hoc empty we end up with Task<[string, number], string | empty>, and then have to do something like this:
const fixEmpty = <T>(x: empty | T): T => (x: any)
result.run({
failure(error) {
console.log(`error: ${fixEmpty(error)}`)
},
})
So it would be great to have a special type empty, that wouldn't have any possible values, and also would have special features empty | T = T, empty & T = empty.
@rpominov yes, I had exactly the same problem, but have totally forgotten.
We actually do have an empty type internally, but it's not exposed. I've had it in the back of my mind to expose an empty type, so thanks for the prodding.
The easiest solution would be to simply expose the type, which is a subtype of all types. @rpominov your process.exit example wouldn't work out of the box with that. Flow models that kind of behavior as an abnormal exit, which is a kind of side effect we can't express in function types at the moment.
(Edit: also in your switch/case example, the type of t in the default branch is indeed empty currently)
(Edit: also in your switch/case example, the type of t in the default branch is indeed empty currently)
Yeah, but it behaves as if it wasany (for example, it shows up in coverage output)
One more use case for empty type is as type parameter to restrict calling some methods.
For example, we have a mutable container:
declare class Container<T> {
put(val: T);
get(): T;
}
Since it's mutable, T is invariant. We can fix this by splitting T into in and out types:
declare class Container<-In, +Out> {
put(val: In);
get(): Out;
}
If we have such a declaration, we can make the type read-only or write-only by using empty as a a parameter.
type ReadOnlyContainer<+T> = Container<empty, T>;
Although this makes sense for In parameters only.
Landed in c603505583993aa953904005f91c350f4b65d6bd
Most helpful comment
Landed in c603505583993aa953904005f91c350f4b65d6bd