How do I check that a switch block is exhaustive i

2020-02-02 10:27发布

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?

标签: typescript
6条回答
ゆ 、 Hurt°
2楼-- · 2020-02-02 10:54

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.

查看更多
We Are One
3楼-- · 2020-02-02 11:05

What I do is to define an error class:

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

and then throw this error in the default case:

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'
            throw new UnreachableCaseError(dataType);
    }
}

I think it's easier to read because the throw clause has the default syntax highlighting.

查看更多
beautiful°
4楼-- · 2020-02-02 11:11

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("How did we get here?");
  }
}

You can see it in action here in TS Playground

查看更多
来,给爷笑一个
5楼-- · 2020-02-02 11:15

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.

查看更多
够拽才男人
6楼-- · 2020-02-02 11:19

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 ...

查看更多
【Aperson】
7楼-- · 2020-02-02 11:20

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 :)
}
查看更多
登录 后发表回答