Search code examples
typescriptvariantdiscriminated-union

Why do exhaustiveness checks work differently for union types?


I'm using exhaustiveness checking as described in TypeScript Deep Dive

Exhaustiveness checks seem to work differently for union types as compared to types that are not union types. Why??

For example, in the code below, note that exhaustivenessCheck1 only works (doesn't type error when it shouldn't) if we assert that x.kind is never.

However, exhaustivenessCheck2 only does the right thing if we assert that x is never.

type Variant1 = {
    kind: 1 | 2
}

type Variant2 = {
    kind: 1
} | {
    kind: 2
}

const x: Variant1 = { kind: 1 };


function exhaustivenessCheck1(x: Variant1) {
    switch (x.kind) {
        case 1:
        case 2:
            break;
        default:
            const _x: never = x.kind; // OK
            const _y: never = x; // Error
    }
}

function exhaustivenessCheck2(x: Variant2) {
    switch (x.kind) {
        case 1:
            break;
        case 2:
            break;
        default:
            const _x: never = x.kind; // Error
            const _y: never = x; // OK
    }
}

TypeScript Playground link (be sure to enable "strict null checks")


Solution

  • Typescript narrows unions when you use a type-guard. The confusion arises from where the union is.

    In Variant1 the union is on the kind member so typescript narrows that union down to never on the default branch. This means x is still of type Variant1, and kind is still accessible on x, just that the type of kind at this point is never

    In Variant2 the union is on the x parameter itself, so x is what gets narrowed. This version is also called a discriminated union with kind being the discriminator. Since all kinds have been checked, on default x will have been narrowed to never and thus accessing kind becomes an error.