typescript: exhaustive switch statements

2021-10-09

 | 

~2 min read

 | 

363 words

How do you tell Typescript that you’re covering all of the cases with your switch statement and there’s nothing else that could happen? By using the never type!

Let’s take a look.

Set Up

Let’s start by setting up our example. We’ll use a discriminated union of employee types:

enum Role {
  Standard = "Standard",
  Admin = "Admin",
}

type Standard = {
  role: Role.Standard
  name: string
  age: number
}

type Admin = {
  role: Role.Admin
  name: string
  securityLevel: "Normal" | "Elevated" | "High"
}

type Employee = Admin | Standard

Switch Exhaustion

Now that we know what our data looks like, imagine we want to pull out the unique attributes for each of our employees.

We might write a switch statement that looks at the role to determine which type of employee we’re dealing with.

This might look like:

function getUniqueAttribute(employee: Employee) {
  switch (employee.role) {
    case Role.Standard:
      return employee.age
    case Role.Admin:
      return employee.access
    default:
      return
  }
}

At this point, you may or may not compile depending on your settings. We know that it’s exhaustive because we’re switching on the Role and we have them all.

But, what if a new role were added later? How could we make sure that our function caught it?

We could add an exhaustive check, like so:

function getUniqueAttribute(employee: Employee) {
  switch (employee.role) {
    case Role.Standard:
      return employee.age
    case Role.Admin:
      return employee.access
    default:
      const _exhaustiveCheck: never = employee
      return _exhaustiveCheck
  }
}

Abstracting The Check

This kind of logic, however, is something I found myself writing over and over since I commonly use a switch to discriminate my types.

A small predicate function to handle this might look like:

export function assertUnreachable(_x: never): never {
  throw new Error("Didn't expect to get here")
}

Then, we can update the switch statement accordingly:

function getUniqueAttribute(employee: Employee) {
  switch (employee.role) {
    case Role.Standard:
      return employee.age
    case Role.Admin:
      return employee.access
    default:
      assertUnreachable(employee)
  }
}

In this case we don’t need to return assertUnreachable because it’ll throw an error - which we can confidently do as our type system will prevent any other type at compilation time.



Hi there and thanks for reading! My name's Stephen. I live in Chicago with my wife, Kate, and dog, Finn. Want more? See about and get in touch!