When flow decides something is empty, this generally means one of two things: either flow has deduced this code is unreachable, or there is a bug in flow -- either it is incorrectly deducing the empty type, or failing to flag an error in a function whose type it has correctly determined is impossible to implement. Either way, the code is effectively untyped: flow will (correctly) let you do anything to empty because it supposedly can't exist in the first place. (It's not quite as bad as any of course, but unlike any, there is no explicit any annotation, nor even a missing flow-type, that you can look for.) This means you have to be looking very closely to spot these places where flow is not providing you type-safety.
I'd love for flow to report places where you try to do something with empty. It could report that the code is unreachable (as #4889 suggests) but I suspect it may be even more confusing if you have code (as in #7300 or #7302) which is obviously not unreachable but which still ends up empty. So it's not obvious to me what the right error message(s) would be, or how to tell which to use. But in any case, the current situation is super scary to me as it means we don't even know what code is untyped. (In at least some such cases (such as the example in #7302) it reports the offending lines as covered, which is perhaps technically correct but not very useful.)
This could also be useful for code where the "unreachable" case is intentional: perhaps it handles unexpected data whose type does not match the flow type we have claimed. In this case, it would make sense for Flow to suggest that probably treating the value as some specific type is dangerous, and you should cast it to mixed or any or something to be clear as to how defensively you are handling this error-case.
empty is useful in the following scenario:
// @flow
type Dog = {
name: string,
species: 'dog',
}
type Cat = {
name: string,
species: 'cat',
}
type Hamster = {
name: string,
species: 'hamster',
}
type Pet =
| Dog
| Cat
| Hamster
const makeSound = (animal: Pet) => {
switch (animal.species) {
case 'dog': return 'woof'
case 'cat': return 'miaow'
case 'hamster': return 'squeak'
default: (animal: empty)
}
}
_(try Flow)_
The default clause's type assertion is useful to ensure that we've exhaustively handled all the possible cases (briefly mentioned only in the section on typing Redux reducers). If we later add Goldfish to the Pet union, but forget to update makeSound, Flow would warn us (Cannot cast `animal` to empty because `Goldfish` [1] is incompatible with empty [2].).
I agree that it would be useful for Flow to warn if you tried to do anything _other than a type assertion_ with the animal argument.
Consider implementing it as a linting rule, then one could disable it on a case-by-case basis, e.g. if you'd like to throw it as part of an exception, or log it to the console.
Most helpful comment
emptyis useful in the following scenario:_(try Flow)_
The
defaultclause's type assertion is useful to ensure that we've exhaustively handled all the possible cases (briefly mentioned only in the section on typing Redux reducers). If we later addGoldfishto thePetunion, but forget to updatemakeSound, Flow would warn us (Cannot cast `animal` to empty because `Goldfish` [1] is incompatible with empty [2].).I agree that it would be useful for Flow to warn if you tried to do anything _other than a type assertion_ with the
animalargument.Consider implementing it as a linting rule, then one could disable it on a case-by-case basis, e.g. if you'd like to throw it as part of an exception, or log it to the console.