TypeScript supports exhaustiveness checking for enums and similar types. This blog post shows how to use idiomatic JavaScript for this kind of check.
Consider the following enum:
enum NoYes {
No = 'No',
Yes = 'Yes',
}
The switch
statement in the following code is idiomatic JavaScript:
function toGerman(x: NoYes) {
switch (x) {
case NoYes.No:
return 'Nein';
case NoYes.Yes:
return 'Ja';
default:
throw new UnsupportedValueError(x); // (A)
}
}
Can we also get static errors in TypeScript?
In line A, the inferred type of x
is never
because we have already taken care of all values that it can have. Therefore, we can instantiate the following exception in line A:
class UnsupportedValueError extends Error {
constructor(value: never) {
super('Unsupported value: ' + value);
}
}
If, however, we forget one of the switch
cases, then the type of x
isn’t never
anymore and we get a static error:
function toGerman(x: NoYes) {
switch (x) {
case NoYes.Yes:
return 'Ja';
default:
//@ts-ignore: Argument of type 'NoYes.No' is not assignable to parameter of type 'never'. (2345)
throw new UnsupportedValueError(x);
}
}
if
statements TypeScript also warns us if we use if
statements:
function toGermanNonExhaustively(x: NoYes) {
if (x === NoYes.Yes) {
return 'Ja';
} else {
// @ts-ignore: Argument of type 'NoYes.No' is not assignable to parameter of type 'never'. (2345)
throw new UnsupportedValueError(x);
}
}
function toGermanExhaustively(x: NoYes) {
if (x === NoYes.No) {
return 'Nein';
} else if (x === NoYes.Yes) {
return 'Ja';
} else {
throw new UnsupportedValueError(x); // No warning
}
}
Instead of using a default case, we can also specify a return type. Then TypeScript warns us if we forget a case (because we implicitly return undefined
):
enum NoYes {
No = 'No',
Yes = 'Yes',
}
//@ts-ignore: Function lacks ending return statement and return type does not include 'undefined'. (2366)
function toGerman(x: NoYes): string {
switch (x) {
case NoYes.Yes:
return 'Ja';
}
}
if
statements Alas, with this approach, we get a warning even if we exhaustively handle all cases (see second function, toGermanExhaustively()
):
// @ts-ignore: Function lacks ending return statement and return type does not include 'undefined'. (2366)
function toGermanNonExhaustive(x: NoYes): string {
if (x === NoYes.Yes) {
return 'Ja';
}
}
// @ts-ignore: Function lacks ending return statement and return type does not include 'undefined'. (2366)
function toGermanExhaustive(x: NoYes): string {
if (x === NoYes.No) {
return 'Nein';
} else if (x === NoYes.Yes) {
return 'Ja';
}
}
How does this approach compare to using an exception?
if
statementsThis pattern works for: