-
-
Notifications
You must be signed in to change notification settings - Fork 2.9k
Description
I frequently see code like this:
type MyUnion = 'a' | 'b';
declare const myUnion: MyUnion;
const f1 = () => (myUnion === 'a' ? 'a' : 'b');
f1('a') // 'a'
f1('b') // 'b'The logic in f1 makes an assumption: if myUnion is not a, it must be b.
Later on, someone might update the MyUnion type and this assumption will breakdown:
-type MyUnion = 'a' | 'b';
+type MyUnion = 'a' | 'b' | 'c';The runtime behaviour is clearly incorrect, yet TypeScript will not error to remind us that we need to update the logic in f1:
f1('a') // 'a'
f1('b') // 'b'
f1('c') // 'b' ❌This problem is not specific to the ternary operator but also if and switch statements:
const f2 = () => {
if (myUnion === 'a') {
return 'a';
} else {
return 'b';
}
};
const f3 = () => {
switch (myUnion) {
case 'a':
return 'a';
default:
return 'b';
}
};As we can see, it is not safe to make assumptions about the value that reaches the else/default case because it can change.
Instead we need to explicitly specify all cases:
import assertNever from 'assert-never';
type MyUnion = 'a' | 'b';
declare const myUnion: MyUnion;
const f2 = () => {
if (myUnion === 'a') {
return 'a';
} else if (myUnion === 'b') {
return 'b';
} else {
assertNever(myUnion);
}
};
const f3 = () => {
switch (myUnion) {
case 'a':
return 'a';
case 'b':
return 'b';
}
};
const f3b = () => {
switch (myUnion) {
case 'a':
return 'a';
case 'b':
return 'b';
default:
assertNever(myUnion);
}
};This way, when the type is eventually widened, TypeScript will generate a type error so we're reminded that we need to update our code:
import assertNever from 'assert-never';
type MyUnion = 'a' | 'b' | 'c';
declare const myUnion: MyUnion;
const f2 = () => {
if (myUnion === 'a') {
return 'a';
} else if (myUnion === 'b') {
return 'b';
} else {
// Argument of type 'string' is not assignable to parameter of type 'never'.
assertNever(myUnion);
}
};
// @noImplicitReturns: true
// Not all code paths return a value.
const f3 = () => {
switch (myUnion) {
case 'a':
return 'a';
case 'b':
return 'b';
}
};
const f3b = () => {
switch (myUnion) {
case 'a':
return 'a';
case 'b':
return 'b';
default:
// Argument of type 'string' is not assignable to parameter of type 'never'.
assertNever(myUnion);
}
};I would like to propose a rule that enforces this. The rule would report an error inside a ternary or if/switch statement if we're switching over a union type (except boolean) and we have a fallback case (else/default). The fix would be to explicitly specify all cases.
I'm really not sure what we would call it.
WDYT?