Compared to some other languages,Flow’s story around exhaustiveness checkingwithinif / else andswitch statements leavessomething to be desired. By default, Flow doesn’t do any exhaustivenesschecks! But wecan opt-in to exhaustiveness checkingone statement at a time.
In this post, we’ll discover from the ground up how Flow’sexhaustiveness checking behaves. But if you’re just looking for theresult, here’s a snippet:
TL;DR
type A= {tag:"A"};type B= {tag:"B"};type AorB= A| B;const absurd=<T>(x: empty): T=> {thrownewError('This function will never run!');}const allGood= (x: AorB): string=> {if(x.tag==="A") {return"In branch A"; }elseif (x.tag==="B") {return"In branch B"; }else {returnabsurd(x); }}const forgotTagB= (x: AorB): string=> {if(x.tag==="A") {return"In branch A"; }else {// B. This type is incompatible with the expected param type of empty.returnabsurd(x); }}How Exhaustiveness Behavesin Flow
Here we have a typeAorB with two variants;
type A= {tag:"A"};type B= {tag:"B"};type AorB= A| B;const fn1= (x: AorB): string=> {if(x.tag==="A"){return"In branch A"; }else {return"In branch B"; }}All well and good, but what if we add a new case? For example, whatif we take the snippet above and add this:
type C= {tag:"C"};type AorBorC= A| B| C;const fn2= (x: AorBorC): string=> {if(x.tag==="A") {return"In branch A"; }else {return"In branch B"; }}Wait a second, it type checks!
That’s because we used a catch-allelse branch. What ifwe make each branch explicit?
// ERROR: ┌─▶︎ string. This type is incompatible with an implicitly-returned undefined.const fn3= (x: AorBorC): string=> {if(x.tag==="A") {return"In branch A"; }elseif (x.tag==="B") {return"In branch B"; }}Phew, so it’s reminding us that we’re not covering all the cases.Let’s add the newC case:
// ERROR: ┌─▶︎ string. This type is incompatible with an implicitly-returned undefined.const fn4= (x: AorBorC): string=> {if(x.tag==="A") {return"In branch A"; }elseif (x.tag==="B") {return"In branch B"; }elseif (x.tag==="C") {return"In branch C"; }}Hmm: it still thinks that we might returnundefined,even though we’ve definitely covered all the cases… 🤔
What wecan do is add a default case, but ask Flowtoprove that we can’t get there, using Flow’sempty type:
const fn5= (x: AorBorC): string=> {if(x.tag==="A") {return"In branch A"; }elseif (x.tag==="B") {return"In branch B"; }elseif (x.tag==="C") {return"In branch C"; }else { (x: empty);thrownewError('This will never run!'); }}Thethrow new Error line above will never run, becauseit’s not possible to construct a value of typeempty.empty if you useany! Moral of the story: bevery diligent about eradicatingany.
(“There are no values in the empty set.”)
If we adopt this pattern everywhere, we’d see this error message ifwe forgot to add the new case forC:
const fn6= (x: AorBorC): string=> {if(x.tag==="A") {return"In branch A"; }elseif (x.tag==="B") {return"In branch B"; }else {// C. This type is incompatible with empty. (x: empty);thrownewError('absurd'); }}Flow tells us “Hey, I found a C! So I couldn’t prove that this switchwas exhaustive.”
But this pattern is slightly annoying to use, because ESLintcomplains:
no-unused-expressions: Expected an assignment or function call and instead saw an expression.We can fix this by factoring thatempty ... throwpattern into a helper function:
// 'absurd' is the name commonly used elsewhere for this function. For example:// https://hackage.haskell.org/package/void-0.7.1/docs/Data-Void.html#v:absurdconst absurd=<T>(x: empty): T=> {thrownewError('absurd');};const fn7= (x: AorBorC): string=> {if(x.tag==="A") {return"In branch A"; }elseif (x.tag==="B") {return"In branch B"; }elseif (x.tag==="C") {return"In branch C"; }else {returnabsurd(x); }}const fn8= (x: AorBorC): string=> {if(x.tag==="A") {return"In branch A"; }elseif (x.tag==="B") {return"In branch B"; }else {// C. This type is incompatible with the expected param type of empty.returnabsurd(x); }}So there you have it! You can put that helper function(absurd) in a file somewhere and import it anywhere. Youcould even give it a different name if you want! I’ve been using thispattern in all the Flow code I write these days and it’s been nice torely on it when doing refactors.