Skip to content

Exhaustiveness check does not work for tagged union when there is the only tag #38963

Closed
@BobNobrain

Description

@BobNobrain

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Working as IntendedThe behavior described is the intended behavior; this is not a bug

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions