Making sure a switch statement is exhaustive in TypeScript

Posted on 2020-02-04

When working with union types it is very common that you add additional types while your software evolves.

Let's take this union type:

interface AnswerYes {
    result: "yes";
}

interface AnswerNo {
    result: "no";
}

type Answer = AnswerYes | AnswerNo;

and do a switch on that:

export function printToConsole(answer: Answer): void {
    switch (answer.result) {
        case "yes":
            console.log("YES");
            break;
        case "no":
            console.log("NO");
            break;
    }
}

If you add another type to your union the code above will fail to handle that case and the compiler won't help you.

interface AnswerMaybe {
    result: "maybe";
}

type Answer = AnswerYes | AnswerNo | AnswerMaybe;

To fix this issue we can use the following utility function:

export function assertUnreachable(x: never): never {
    throw new Error("assertUnreachable: " + x);
}

This function takes a parameter of type never as its argument, so you can only call it if typescript infers that type.

If you use that as the default case in your switch statement like this

export function printToConsole(answer: Answer): void {
    switch (answer.result) {
        case "yes":
            console.log("YES");
            break;
        case "no":
            console.log("NO");
            break;
        default:
            assertUnreachable(answer); // Compiler error
    }
}

you'll get an error after adding the AnswerMaybe to the union type, so the compiler will tell you exactly which cases you'll still need to handle.