Repository navigation
Exhaustiveness check does not work for tagged union when there is the only tag #38963
Description
TypeScript Version: 3.9.2
Search Terms:
- typescript exhaustiveness check for single tag
- typescript exhaustiveness check for single option
- typescript switch case on tagged union with single item
- typescript type guards for tagged union of single item
Expected behavior:
No compile errors: we cannot get the default branch, so type of x in there should be narrowed down to never, and the function test is correct.
Actual behavior:
Compile error at line 20 (return absurd(x);):
Argument of type 'Disjoint' is not assignable to parameter of type 'never'.
However, if we uncomment the code that adds the second item to Disjoint, it will be working properly.
Code
enum Labels {RED, YELLOW}
type Disjoint
= { label: Labels.RED; value: number }
// | { label: Labels.YELLOW; value: string}
;
const absurd = (u: never): never => { throw new Error() };
function test(x: Disjoint): number {
switch (x.label) {
case Labels.RED:
return x.value;
// case Labels.YELLOW:
// return +x.value;
default:
return absurd(x);
}
}This may seem a weird corner-case, but I have stumbled this behaviour multiple times. You can have a tagged union of single tag when you plan to extend it later. When you want to ensure that later when you add new tags to it, you will get compile errors in every non-exhaustive switch statement that must be exhaustive.
Output
"use strict";
var Labels;
(function (Labels) {
Labels[Labels["RED"] = 0] = "RED";
Labels[Labels["YELLOW"] = 1] = "YELLOW";
})(Labels || (Labels = {}));
const absurd = (u) => { throw new Error(); };
function test(x) {
switch (x.label) {
case Labels.RED:
return x.value;
// case Labels.YELLOW:
// return +x.value;
default:
return absurd(x);
}
}Compiler Options
{
"compilerOptions": {
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictPropertyInitialization": true,
"strictBindCallApply": true,
"noImplicitThis": true,
"noImplicitReturns": true,
"useDefineForClassFields": false,
"alwaysStrict": true,
"allowUnreachableCode": false,
"allowUnusedLabels": false,
"downlevelIteration": false,
"noEmitHelpers": false,
"noLib": false,
"noStrictGenericChecks": false,
"noUnusedLocals": false,
"noUnusedParameters": false,
"esModuleInterop": true,
"preserveConstEnums": false,
"removeComments": false,
"skipLibCheck": false,
"checkJs": false,
"allowJs": false,
"declaration": true,
"experimentalDecorators": false,
"emitDecoratorMetadata": false,
"target": "ES2017",
"module": "ESNext"
}
}Playground Link: Provided
Activity
RyanCavanaugh commentedon Jun 10, 2020
We've discussed narrowing-to-never of nonunions (singletons) in other issues and it's unfortunately not a great way forward. There are a few issues with doing this. The first is performance; for non-unions we simply don't do any narrowing logic like this in the first place. The second is that immediately going to
neverraises a lot of problems in otherwise innocuous code if you start assuming that any observation outside the type system impliesnever; you can see code fairly frequently like this:I don't have a super-great workaround off the top of my head that keeps the single-case
switchstructure in place.BobNobrain commentedon Jun 10, 2020
Ok, thank you for detailed answer. I didn't realize there can exist examples like this because I was lucky enough to not see anything like that =) This in theory can be solved with another
--strictNNNflag, but performance issue is surely a showstopper here.I suppose I should close this now, as this behaviour will not be changed.
fatcerberus commentedon Jun 19, 2020
Ryan Cavanaugh (@RyanCavanaugh) Doesn't discriminated-union narrowing only apply when the property being checked is a unit type, though? In your example
arg.xisstring, so.xwouldn't be a discriminator there anyway. And if.xwere a unit type, then it seems fair to assume such a check wouldn't exist unless you wanted the narrowing.