How do I check that a switch block is exhaustive in TypeScript?
typescript-eslint
has "exhaustiveness checking in switch with union type" rule:
@typescript-eslint/switch-exhaustiveness-check
To configure this, enable the rule in package.json
and enable the TypeScript parser. An example that works with React 17:
"eslintConfig": {
"rules": {
"@typescript-eslint/switch-exhaustiveness-check": "warn"
},
"parser": "@typescript-eslint/parser",
"parserOptions": {
"project": "./tsconfig.json"
}
},
To do this, we'll use the never
type (introduced in TypeScript 2.0) which represents values which "shouldn't" occur.
First step is to write a function:
function assertUnreachable(x: never): never {
throw new Error("Didn't expect to get here");
}
Then use it in the default
case (or equivalently, outside the switch):
function getColorName(c: Color): string {
switch(c) {
case Color.Red:
return 'red';
case Color.Green:
return 'green';
}
return assertUnreachable(c);
}
At this point, you'll see an error:
return assertUnreachable(c);
~~~~~~~~~~~~~~~~~~~~~
Type "Color.Blue" is not assignable to type "never"
The error message indicates the cases you forgot to include in your exhaustive switch! If you left off multiple values, you'd see an error about e.g. Color.Blue | Color.Yellow
.
Note that if you're using strictNullChecks
, you'll need that return
in front of the assertUnreachable
call (otherwise it's optional).
You can get a little fancier if you like. If you're using a discriminated union, for example, it can be useful to recover the discriminant property in the assertion function for debugging purposes. It looks like this:
// Discriminated union using string literals
interface Dog {
species: "canine";
woof: string;
}
interface Cat {
species: "feline";
meow: string;
}
interface Fish {
species: "pisces";
meow: string;
}
type Pet = Dog | Cat | Fish;
// Externally-visible signature
function throwBadPet(p: never): never;
// Implementation signature
function throwBadPet(p: Pet) {
throw new Error('Unknown pet kind: ' + p.species);
}
function meetPet(p: Pet) {
switch(p.species) {
case "canine":
console.log("Who's a good boy? " + p.woof);
break;
case "feline":
console.log("Pretty kitty: " + p.meow);
break;
default:
// Argument of type 'Fish' not assignable to 'never'
throwBadPet(p);
}
}
This is a nice pattern because you get compile-time safety for making sure you handled all the cases you expected to. And if you do get a truly out-of-scope property (e.g. some JS caller made up a new species
), you can throw a useful error message.
You don't need to use never
or add anything to the end of your switch
.
If
- Your
switch
statement returns in each case - You have the
strictNullChecks
typescript compilation flag turned on - Your function has a specified return type
- The return type is not
undefined
orvoid
You will get an error if your switch
statement is non-exhaustive as there will be a case where nothing is returned.
From your example, if you do
function getColorName(c: Color): string {
switch(c) {
case Color.Red:
return 'red';
case Color.Green:
return 'green';
// Forgot about Blue
}
}
You will get the following compilation error:
Function lacks ending return statement and return type does not include
undefined
.
Building on top of Ryan's answer, I discovered here that there is no need for any extra function. We can do directly:
function getColorName(c: Color): string {
switch (c) {
case Color.Red:
return "red";
case Color.Green:
return "green";
// Forgot about Blue
default:
const exhaustiveCheck: never = c;
throw new Error(`Unhandled color case: ${exhaustiveCheck}`);
}
}
You can see it in action here in TS Playground
Edit: Included suggestion to avoid "unused variable" linter messages.