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