Somewhere out there is a production deploy with if (status === 'nominaded') sitting in a
conditional, silently doing nothing, because nobody caught the typo and plain JavaScript will
happily compare a string to a string all day long, right or wrong. It compiled. It shipped. It
just never matched anything. This week's job is making that particular bug impossible to write.
Unions: type-level OR
You already know || at the value level: a || b picks whichever value is truthy. A union type
is the same idea one level up, at the type level: string | null means "this value is a string,
or it is null, nothing else." You reach for that one constantly already, probably without a name
for it. Literal types push the idea further: instead of the general type string, a literal type
like 'won' is the type whose only valid value is the exact string "won". Union a few literals
together and you get a closed set of exact strings, which is a lot more specific than string.
The ES6 you write
let compResult = 'won'
function announceResult(result) {
return result === 'won' ? 'HOH secured' : 'better luck next time'
}The TS version
type CompResult = 'won' | 'lost' | 'forfeited'
let compResult: CompResult = 'won'
function announceResult(result: CompResult): string {
return result === 'won' ? 'HOH secured' : 'better luck next time'
}What changed
compResult goes from the general type string, which happily accepts any string including a typo, to the union 'won' | 'lost' | 'forfeited', which accepts only those three exact strings. Try to assign compResult = 'wonn' on the right and TypeScript rejects it before you ever run the code.
The never trick: proof you handled every case
A union of three literals means exactly three values are possible, no more. That gives TypeScript
something plain JavaScript never had: it can tell whether a switch over that union covers every
case. Here is the trick, and it is worth understanding rather than memorizing.
function describeResult(result: CompResult): string {
switch (result) {
case 'won':
return 'HOH secured'
case 'lost':
return 'better luck next time'
default: {
const impossible: never = result
return impossible
}
}
}That switch is missing a case for 'forfeited'. Inside the default branch, TypeScript narrows
result down to whatever is left after every handled case is subtracted, which here is still
'forfeited', one real, live value. Assigning a real value to a variable typed never, the type
with no valid values at all, is a compile error. So the missing case shows up as a red squiggle on
that assignment, at the exact spot the bug lives, not as a mystery bug report three weeks later.
Add the 'forfeited' case and result inside default genuinely has nothing left to be, TypeScript
narrows it all the way to never, and the assignment compiles clean. That is the whole trick: the
compiler proves you handled every case by trying, and failing, to find a value that could still
reach default.
That is the whole lesson. Now prove it in the comp below.