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

前端 未结 10 897
伪装坚强ぢ
伪装坚强ぢ 2020-12-02 10:57

I have some code:

enum Color {
    Red,
    Green,
    Blue
}

function getColorName(c: Color): string {
    switch(c) {
        case Color.Red:
                     


        
相关标签:
10条回答
  • 2020-12-02 11:19

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

    0 讨论(0)
  • 2020-12-02 11:21

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

    0 讨论(0)
  • 2020-12-02 11:22

    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 :)
    }
    
    0 讨论(0)
  • 2020-12-02 11:24

    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.

    0 讨论(0)
  • 2020-12-02 11:26

    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.

    0 讨论(0)
  • 2020-12-02 11:33

    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

    0 讨论(0)
提交回复
热议问题