200

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?

Ryan Cavanaugh
  • 209,514
  • 56
  • 272
  • 235
  • 4
    Just wanted to inform people there's a two-line solution from @Carlos Gines if you scroll down far enough. – Noumenon May 15 '20 at 17:34
  • 2
    TypeScript's `noImplicitReturns` can help with this. – Emil Laine Jan 29 '21 at 14:06
  • Am I missing something obvious? Just removing the `throw` is enough to get a compiler error for me https://www.typescriptlang.org/play?ts=5.2.2#code/KYOwrgtgBAwg9gGzgJygbwFBSgJWAEwBosoBxZYUY7AIQTGGIF8MMAzMEAYwBcBLOCCgBzYD3hJkAOQCGEYAAouALliIUASlUBnHsj4hh6Etm0B3Pjy4ALJRuPZHjrjO3A1kgHR58yk04CKHjBkIQByCnwwgG5-Jxc3DxRPckoQPwDMqCCQ8OEKUBj-FhYgA and adding the missing Blue case makes it go away. This seems to hold across TS 3, TS 4, TS 5. Edit: I can see @Marcelo Lazaroni already posted this answer – Anentropic Aug 30 '23 at 11:30

14 Answers14

222

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.

Ryan Cavanaugh
  • 209,514
  • 56
  • 272
  • 235
  • 12
    With `strictNullChecks` enabled, isn't it enough to define the function's return type as `string`, without needing an `assertUnreachable` function at all? – dbandstra Jun 22 '17 at 21:50
  • 2
    @dbandstra You can certainly do that, but as a generic pattern, the assertUnreachable is more dependable. It works even without `strictNullChecks` and also continues to work if there are conditions where you want to return undefined from outside the switch. – Letharion Jun 26 '17 at 08:37
  • This doesn't seem to work when the enum is defined in an external d.ts file. At least I can't get it to work with the enum Office.MailboxEnums.RecipientType from Microsofts Office JS add-in API. – Søren Boisen Sep 18 '18 at 21:52
  • the above solution works for the basic example in the original description, however, I am not certain that it's a generally applicable pattern. Swap out the color enum with `type InDiscriminant = { a: string } | { b: string }` and how can one proceed? A simple switch no longer knows all of the stringy values, and there aren't common properties among member types of the union to discriminate over. Is there an idiomatic solution for this case? – cdaringe Nov 09 '19 at 17:13
  • 1
    `noUnusedParameters` tsconfig set up will throw a "x is never used error" which you can fix by prefixing with underscore `function assertUnreachable(_x: never): never { throw new Error("Didn't expect to get here"); }` – jbmilgrom Jan 30 '20 at 21:32
  • Also be careful with the spread operator. Spreading will break these type unions and typescript will throw an error regardless if you have an exhaustive check or not. – fengelhardt Dec 28 '20 at 11:09
  • 4
    If you use this with a string union type `type Pet = "Cat"|"Dog"|"Fish"` the error will only say that string is not assignable to never. Any way to get the name of the missing string in the error message? – Thomas Oster Feb 09 '21 at 09:39
  • 1
    For whatever reason mine will catch the missing case, but it doesn't provide a helpful error, it just complains about the import but doesn't explain which case is not being met. ie: `Type 'import("/color.enum").Color' is not assignable to type 'never'.ts(2322)` I ended up going with a mapping/dictionary lookup approach since it does seem to provide the missing case. I imagine this may vary from Typescript version. – CTS_AE Aug 18 '23 at 14:09
102

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.

Albert Vila Calvo
  • 15,298
  • 6
  • 62
  • 73
Carlos Ginés
  • 1,192
  • 1
  • 7
  • 9
  • 3
    I prefer using variable too, instead of creating a function (though both might get stripped at bundle time). In any case, your linter may complain about an unused variable, and you'll have to add something like this above it: `// eslint-disable-next-line @typescript-eslint/no-unused-vars` – Matthias Oct 17 '19 at 18:04
  • 9
    Or, you can use the variable in the error you throw, something like: `throw new Error(\`Unexpected: ${_exhaustiveCheck}\`);` – Nooodles Jan 07 '20 at 08:19
  • 1
    Nice idea. You could also define an anonymous function: `default: ((x: never) => { throw new Error(c + " was unhandled."); })(c);` – Brady Holt May 14 '20 at 19:39
  • I can confirm that @Nooodles solution satisfies SonarQube unused variables check. Thanks. Love how terse this is. – Noumenon May 15 '20 at 17:18
  • 2
    I addressed some related (I think these might be pretty frequent) errors from the code above like this: ```default: { const pleaseBeExhaustive: never = c; throw new Error(`Use all Color - ${pleaseBeExhaustive as string}`); }``` – Jonny Sep 23 '20 at 09:13
  • I can confirm that @Jonny's braces creating a scope around the default statement are necessary to prevent a `no-case-declarations` error from ESLint. Also, in PyCharm you need `// noinspection UnnecessaryLocalVariableJS` over the intermediate variable. Still, this is the best way to get a compile time error you can see. – Noumenon Sep 14 '22 at 03:43
  • 2
    In TS 4.9 this simplifies to `c satisfies never`. No assignment, and no unused variable. – huw Jan 24 '23 at 04:23
86

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.

Marcelo Lazaroni
  • 9,819
  • 3
  • 35
  • 41
  • This solution require to explicitly specify all possible return values of a function. In other cases we can omit them and let compiler do type inference. – Aleksei Jun 19 '19 at 13:28
  • 1
    yes, the compiler will complain, but you should also consider, that you could still get a wrong value at runtime. In such a case, I prefer to throw a meaningful error-message (instead of implicitly returning undefined) – TmTron Jun 19 '19 at 16:38
  • 11
    The idea is **not** to return `undefined`, but to create the missing branch of the case statement. This way you would not get an error at runtime and there is no need to throw anything. – Marcelo Lazaroni Jun 20 '19 at 10:42
  • 1
    @MarceloLazaroni your idea is good, but your code does not work that way At runtime there is no guarantee, that `c` is indeed a `Color` - since it is just JavaScript code the function can be called with anything: e.g. `c` could be a number, then your function would return `undefined`: see this [stackblitz example](https://stackblitz.com/edit/so-39419170) – TmTron Jun 29 '19 at 18:40
  • 5
    The code works exactly as described. If you will ignore the type and consider that `c` could be anything there is no reason to use TypeScript at all. If `c` could be null or undefined, the type should say that and the implementation would then take that into account. – Marcelo Lazaroni Jun 30 '19 at 19:20
  • @MarceloLazaroni I think you still don't get my point: typescript does NOT guard against runtime errors: i.e. when your function is called at runtime with anything other than a `Color` (for whatever reason), your function will return `undefined`. This is different to e.g. Java where the JVM would catch this type-error (at runtime) and throw some exception. – TmTron Jul 08 '19 at 07:11
  • 2
    This solution works to trigger a typescript error, but the error is not always clear. I prefer to use a `default` case with an unused variable typed as `never` with a comment explaining `"hey bud, if you're getting an error here, you forgot to add a case"`. YMMV. – Matthias Oct 17 '19 at 18:01
  • @TmTron but if the type of the method argument is ensured at compile time, how could an erroneous type ever be used at runtime? Assuming the source is pure typescript. – S-K' Nov 12 '19 at 13:33
  • 4
    @S-K' when you get some external data: e.g. you request data from the server and then just cast it to your interface. Then the compiler will not complain, but when the server really sends any string instead of your expected enum, you get a runtime error. Or an old version of your app stored some data in local storage and a newer version reads this data - but the interface definition has changed. – TmTron Nov 12 '19 at 13:45
  • @TmTron You're making the assumption that the enum is being mapped to data outside of the programs control. In the scenarios you mentioned, a fail fast approach with `throw new Error()` makes sense. But when the data is within the apps control, an exhaustive switch is great. – S-K' Nov 13 '19 at 15:07
  • @S-K' this can even happen in your own program: e.g. the example with local storage and 2 different app versions, or someone uses a cast, etc. So adding a single throw can help you a lot when something unexpected happens.. – TmTron Nov 13 '19 at 15:26
  • This doesn't work if I have an interface (say, EventSymbolToInterfaceMap) where keys are symbols, and values are interfaces, and define the function like that: `getEventMessage(type: E) { switch (type) ... }`. I still have the "Function lacks ending return statement" error, but all four points from your answer are true for my code. Is it because the function is generic? `assertUnreachable` at the end still works, however, which is strange to me. – 1valdis Sep 30 '21 at 09:41
  • What IF my function is not supposed to return anything? It has a `void` return type, then? – Nawaz Nov 08 '21 at 10:30
  • @Nawaz then you don't meet one of the 4 requirements listed above for the use of this technique. You will need to do something else. – Marcelo Lazaroni Nov 21 '21 at 01:39
  • ah, right. I did not pay attention to the 4 requirements listed in this answer. Thanks for pointing that out. – Nawaz Nov 21 '21 at 15:36
  • 1
    I was able to get this working with discriminated unions by asserting on the object itself instead of the property, like `assertUnreachable(myObj)`, where I'm switching on a property like `switch (myObj.color) {` – frodo2975 Feb 04 '22 at 21:17
  • "when you get some external data: e.g. you request data from the server and then just cast it to your interface [...] this can even happen in your own program: e.g. the example with local storage [...]" -- Following up: the way to address this issue is to not blindly cast/blindly construct the enum from the raw value. The program must validate the raw value before constructing the enum from it. For example, the program could have a function like `colorFromRaw(rawValue: string) => Color` to construct the `Color` enum. The function throws if the input `rawValue` is invalid. – nishanthshanmugham Dec 30 '22 at 15:57
54

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

Tyler2P
  • 2,324
  • 26
  • 22
  • 31
drets
  • 2,583
  • 2
  • 24
  • 38
  • 3
    The ESLint `default` rule requires a default case anyway, which renders this rule useless since a default case makes everything exhaustive. Also the rule is noted as expensive. Note it doesn't work on enums (but union types are better anyway). – Noumenon Sep 14 '22 at 03:12
  • 1
    @Noumenon yes, if there is `default` rule then this rule is useless. It's as expensive as any eslint rule that requires reading types information. – drets Sep 20 '22 at 09:30
40

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

TmTron
  • 17,012
  • 10
  • 94
  • 142
  • note, that you can currently not use `instanceof` checks for subclasses of `Error`: see [issue#13965](https://github.com/Microsoft/TypeScript/issues/13965#issuecomment-278570200) which also mentions a workaround – TmTron Feb 06 '19 at 10:23
  • I checked it and it works correctly now. TypeScript 3.5.1. – Aleksei Jun 19 '19 at 13:26
  • 1
    Ooooh, this is beautiful, and also almost exactly how I would write the code, if I was writing in plain JS. – Pete Jan 17 '20 at 13:10
  • How does the compiler find this? – jocull Mar 18 '20 at 03:04
  • @jocull not sure what you are asking for. Maybe you want to read about [Discriminated Unions](https://www.typescriptlang.org/docs/handbook/advanced-types.html#discriminated-unions)? – TmTron Mar 18 '20 at 07:10
  • I think that has the docs I was looking for - thanks :) [Exhaustiveness checking](https://www.typescriptlang.org/docs/handbook/advanced-types.html#exhaustiveness-checking) – jocull Mar 18 '20 at 18:26
  • Actually there's no need to put `val` into the `Error` message in `UnreachableCaseError`, if you are only after the TS ty-checking. – chpio Aug 03 '20 at 17:21
  • @Lonli-Lokli Not sure what you mean. Throwing an exception in the default case is not related to the type of variable that is used in the switch statement - here is a [Stackblitz example](https://stackblitz.com/edit/unreachable-case-error-string?file=index.ts) that works with a string-enum – TmTron Aug 28 '20 at 16:46
  • Note: this pattern works only if you return something in each case. If you are in a `forEach` loop, you can add `return;` to each case for example. Also, it did not work for a nested property in the switch for me, like `switch(foo.bar.zed)`, instead I did `const zed = foo.bar.zed` beforehand. – Eric Burel May 12 '21 at 14:35
  • 1
    Actually after more try, it doesn't work very well on a nested prop, like `foo.bar.zed`. If you do `const zed = foo.bar.zed; switch(zed)...` you loose the automatic detection of `foo` type based on `zed` (think GeoJSON Geometry, that you can differentiate based on the type). But if you don't do this, `UnreachableError(foo.bar.zed)` won't work – Eric Burel May 12 '21 at 15:44
  • 1
    Finally for nesting: you may need to do `UnreachableError(foo.bar)` in this case, even if you switch on `foob.bar.zed`. – Eric Burel May 12 '21 at 16:04
  • *"Java will also check the types at runtime"* -- This is kind of a weird comparison, as type checks only happen in Java when explicitly downcasting a reference, or when using reflection. There is no runtime check when passing a reference to a function that accepts a compatible reference type, for example; it's assumed that the referent is of a compatible type. (If that's not the case then the type system broke and all bets are off.) The point is, there's usually not that much type checking happening at runtime in Java; it's still mostly compile-time. – cdhowie Sep 05 '21 at 14:44
21

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

huw
  • 902
  • 1
  • 8
  • 14
8

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.

Hadrien Milano
  • 111
  • 1
  • 3
  • 1
    The clearer error message is nice but one small downside is that accidentally passing something with type 'any' won't trigger the error. – Ashley May 27 '21 at 11:08
6

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.

Brady Holt
  • 2,844
  • 1
  • 28
  • 34
4

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
Chris G
  • 49
  • 2
  • 2
    You are not wrong. However, I think throwing an Error is slightly safer here since it doesn't rely on the implementation details of TypeScript in the case of a problem (ie, if your runtime somehow manages to reach the never case despite your types, you'll at least get an error instead of undefined behavior). Since you can lie with typescript (eg by declaring something that is not proven), it's important to throw. – mscottnelson Feb 16 '21 at 22:02
3

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.

Benny Code
  • 51,456
  • 28
  • 233
  • 198
3

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.

2

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 :)
}
Maksim Nesterenko
  • 5,661
  • 11
  • 57
  • 91
0

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

sky
  • 685
  • 7
  • 8
  • This only works if your enum values are strings since they need to be encoded as keys in the object parameter `map`. – Will Madden Sep 09 '19 at 13:43
0

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.

Offirmo
  • 18,962
  • 12
  • 76
  • 97