Ryan Cavanaugh
Ryan Cavanaugh

Reputation: 221212

How do I check that a switch block is exhaustive in TypeScript?

I have some code:

enum Color {
    Red,
    Green,
    Blue
}

function getColorName(c: Color): string {
    switch(c) {
        case Color.Red:
            return 'red';
        case Color.Green:
            return 'green';
        // Forgot about Blue
    }

    throw new Error('Did not expect to be here');
}

I forgot to handle the Color.Blue case and I'd prefer to have gotten a compile error. How can I structure my code such that TypeScript flags this as an error?

Upvotes: 247

Views: 103592

Answers (16)

Deyvidas B
Deyvidas B

Reputation: 29

You can use the satisfies operator in default case

enum Color {
    Red,
    Green,
    Blue,
}

let foo = {color: Color.Blue};

switch (foo.color) {
    case Color.Red:
        console.log('red');
        break;
    case Color.Green:
        console.log('green');
        break;
    default:
        foo.color satisfies never;
        throw new Error(`Color ${foo.color} not intercepted by switch case`);
}

enter image description here

switch (foo.color) {
    case Color.Red:
        console.log('red');
        break;
    case Color.Green:
        console.log('green');
        break;
    case Color.Blue:
        console.log('blue');
        break;
    default:
        foo.color satisfies never;
        throw new Error(`Color ${foo.color} not intercepted by switch case`);
}

enter image description here

Upvotes: 1

user1548072
user1548072

Reputation: 477

Other answers gave good solutions for returning functions. In case the function is not returning (void), we can add a return statement that will trigger a type-checking error when reached:

function visit(expr: Expr): void {
  switch (expr.__type) {
    case "Binary":
      break;
    case "Grouping":
      break;
    case "Literal":
      break;
    default:
      return expr // Missing "Unary", error: "Type 'Unary' is not assignable to type 'void'."
  }
}

Not the friendliest error message, but works.

Upvotes: 0

huw
huw

Reputation: 1556

This is a lot easier in TypeScript 4.9 with the satisfies keyword.

enum Color {
    Red,
    Green,
    Blue
}

function getColorName(c: Color): string {
  switch(c) {
      case Color.Red:
          return 'Red';
      case Color.Green:
          return 'Green';
      case Color.Blue:
          return 'Blue';
      default:
          return c satisfies never;
  }
}

If your checks are exhaustive, c should always be of type never. We ‘assert’ this to the compiler with the satisfies keyword (in essence, telling it that c should be assignable to never, and to error otherwise). If, in the future, you add a new case to the enum, you will get a pure compile-time error.

Under the hood, the default branch will compile to:

default:
    return c;

This is a literal expression, which will only evaluate c. This shouldn’t have an effect on your code, but if c is, for example, a getter on a class, it will evaluate if the default branch ever runs and could have side effects (as it will in the currently accepted answer).

Upvotes: 81

Carlos Ginés
Carlos Ginés

Reputation: 1560

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.

Upvotes: 138

Arkadiusz Adamski
Arkadiusz Adamski

Reputation: 41

You can use the mapped type for this:

enum Color {
  Red,
  Green,
  Blue,
}

type ColorMapper = {
  [Property in Color]: string
}

const colorMap: ColorMapper = {
  [Color.Red]: "red",
  [Color.Green]: "green",
  [Color.Blue]: "blue",
}

function getColorName(c: Color): string {
  return colorMap[c];
} 

After you add a new value to Color you will need to meet ColorMapper requirements.

Upvotes: 4

drets
drets

Reputation: 2805

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"
    }
},

enter image description here

Upvotes: 74

Offirmo
Offirmo

Reputation: 19860

I'd like to add a useful variant dedicated to tagged union types which is a common use case of switch...case. This solution yields:

  • type check at transpilation time
  • also runtime check, because typescript doesn't guarantee us to be bug free + who knows where the data come from?
switch(payment.kind) {

        case 'cash':
            return reduceⵧprocessꘌcash(state, action)

        default:
            // @ts-expect-error TS2339
            throw new Error(`reduce_action() unrecognized type "${payment?.kind}!`)
    }

The 'never' detection comes for free from dereferencing the "never" base type. Since the error is expected if our code is correct, we flip it with // @ts-expect-error so that it fails if our code is incorrect. I'm mentioning the error ID in case it get supported soon.

Upvotes: 0

Benny Code
Benny Code

Reputation: 54890

The easiest way to find a missing case is to activate TypeScript's check for no implicit returns. Just set noImplicitReturns to true in the compilerOptions section of your tsconfig.json file.

Afterwards you have to remove the throw new Error statement from your code, because it will prevent the TypeScript compiler from throwing an error (because your code is already throwing an error):

enum Color {
  Red,
  Green,
  Blue
}

function getColorName(c: Color): string {
  switch (c) {
    case Color.Red:
      return 'red';
    case Color.Green:
      return 'green';
  }
}

With the above code, you will have an implicit return (because if no case matches, the function will return undefined) and TypeScript's compiler will throw an error:

TS2366: Function lacks ending return statement and return type does not include 'undefined'.

I've also made a video which demonstrates it: https://www.youtube.com/watch?v=8N_P-l5Kukk

In addition, I suggest to narrow down the return type of your function. It actually cannot return any string but only a defined set of strings:

function getColorName(c: Color): 'red' | 'blue'

Narrowing your return type can also help you to find missing cases as some IDEs (like VS Code & WebStorm) will show you when you have unused fields.

Upvotes: 3

Brady Holt
Brady Holt

Reputation: 2934

Building on top of Ryan and Carlos' answers, you can use an anonymous method to avoid having to create a separate named function:

function getColorName(c: Color): string {
  switch (c) {
    case Color.Red:
      return "red";
    case Color.Green:
      return "green";
    // Forgot about Blue
    default:
      ((x: never) => {
        throw new Error(`${x} was unhandled!`);
      })(c);
  }
}

If your switch is not exhaustive, you'll get a compile time error.

Upvotes: 6

Chris G
Chris G

Reputation: 49

To avoid Typescript or linter warnings:

    default:
        ((_: never): void => {})(c);

in context:

function getColorName(c: Color): string {
    switch(c) {
        case Color.Red:
            return 'red';
        case Color.Green:
            return 'green';
        default:
            ((_: never): void => {})(c);
    }
}

The difference between this solution and the others is

  • there are no unreferenced named variables
  • it does not throw an exception since Typescript will enforce that the code will never execute anyway

Upvotes: 4

TmTron
TmTron

Reputation: 19411

Solution

What I do is to define an error class:

export class UnreachableCaseError extends Error {
  constructor(val: never) {
    super(`Unreachable case: ${JSON.stringify(val)}`);
  }
}

and then throw this error in the default case:

enum Color {
    Red,
    Green,
    Blue
}

function getColorName(c: Color): string {
  switch(c) {
      case Color.Red:
          return 'red, red wine';
      case Color.Green:
          return 'greenday';
      case Color.Blue:
          return "Im blue, daba dee daba";
      default:
          // Argument of type 'c' not assignable to 'never'
          throw new UnreachableCaseError(c);
  }
}

I think it's easier to read than the function approach recommended by Ryan, because the throw clause has the default syntax highlighting.

Hint

The ts-essentials library has a class UnreachableCaseError exactly for this use-case

Runtime considerations

Note, that typescript code is transpiled to javascript: Thus all the typescript typechecks only work at compile time and do not exist at runtime: i.e. there is no guarantee that the variable c is really of type Color.
This is different from other languages: e.g. Java will also check the types at runtime and would throw a meaningful error if you tried to call the function with an argument of wrong type - but javascript doesn't.

This is the reason why it is important to throw a meaningful exception in the default case: Stackblitz: throw meaningful error

If you didn't do this, the function getColorName() would implicitly return undefined (when called with an unexpected argument): Stackblitz: return any

In the examples above, we directly used a variable of type any to illustrate the issue. This will hopefully not happen in real-world projects - but there are many other ways, that you could get a variable of a wrong type at runtime.
Here are some, that I have already seen (and I made some of these mistakes myself):

  • using angular forms - these are not type-safe: all form field-values are of type any
    ng-forms Stackblitz example
  • implicit any is allowed
  • an external value is used and not validated (e.g. http-response from the server is just cast to an interface)
  • we read a value from local-storage that an older version of the app has written (these values have changed, so the new logic does not understand the old value)
  • we use some 3rd party libs that are not type-safe (or simply have a bug)

So don't be lazy and write this additional default case - it can safe you a lot of headaches...

Upvotes: 42

Hadrien Milano
Hadrien Milano

Reputation: 111

As a nice twist on Ryan's answer, you can replace never with an arbitrary string to make the error message more user friendly.

function assertUnreachable(x: 'error: Did you forget to handle this type?'): never {
    throw new Error("Didn't expect to get here");
}

Now, you get:

return assertUnreachable(c);
       ~~~~~~~~~~~~~~~~~~~~~
       Type "Color.Blue" is not assignable to type "error: Did you forget to handle this type?"

This works because never can be assigned to anything, including an arbitrary string.

Upvotes: 8

Maksim Nesterenko
Maksim Nesterenko

Reputation: 6213

In really simple cases when you just need to return some string by enum value it's easier (IMHO) to use some constant to store dictionary of results instead of using switch. For example:

enum Color {
    Red,
    Green,
    Blue
}

function getColorName(c: Color): string {
  const colorNames: Record<Color, string> = {
    [Color.Red]: `I'm red`,
    [Color.Green]: `I'm green`,
    [Color.Blue]: `I'm blue, dabudi dabudai`,   
  }

  return colorNames[c] || ''
}

So here you will have to mention every enum value in constant, otherwise you get an error like, for example, if Blue is missing:

TS2741: Property 'Blue' is missing in type '{ [Color.Red]: string; [Color.Green]: string;' but required in type 'Record'.

However it's often not the case and then it's really better to throw an error just like Ryan Cavanaugh proposed.

Also I was a bit upset when found that this won't work also:

function getColorName(c: Color): string {
    switch(c) {
        case Color.Red:
            return 'red';
        case Color.Green:
            return 'green';
    }
    return '' as never // I had some hope that it rises a type error, but it doesn't :)
}

Upvotes: 2

sky
sky

Reputation: 685

Create a custom function instead of using a switch statement.

export function exhaustSwitch<T extends string, TRet>(
  value: T,
  map: { [x in T]: () => TRet }
): TRet {
  return map[value]();
}

Example usage

type MyEnum = 'a' | 'b' | 'c';

const v = 'a' as MyEnum;

exhaustSwitch(v, {
  a: () => 1,
  b: () => 1,
  c: () => 1,
});

If you later add d to MyEnum, you will receive an error Property 'd' is missing in type ...

Upvotes: 0

Marcelo Lazaroni
Marcelo Lazaroni

Reputation: 10247

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 or void

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.

Upvotes: 100

Ryan Cavanaugh
Ryan Cavanaugh

Reputation: 221212

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.

Upvotes: 256

Related Questions