Skip to content

Inconsistency in narrowing behavior between non-union and union #33670

@soul-codes

Description

@soul-codes

TypeScript Version: 3.7.0-dev.20190930

Search Terms: union never switch

Code

Compare the type of the switch argument at the default: branch of the three switches below:

const a = "foo"
switch (a) {
    case "foo": break;
    default: 
        a // never
}

const b = {
    discriminant: "foo" as const
} as { discriminant: "foo" } | { discriminant: "bar" }

switch (b.discriminant){
    case "foo": break;
    case "bar": break;
    default:
        b // never
}

const c = {
    discriminant: "foo" as const
} as { discriminant: "foo" }

switch (c.discriminant){
    case "foo": break;
    default:
        c // object, but was expecting never
}

Expected behavior:
c should be never to be consistent with a and b.

Actual behavior:
c is still the type { discriminant: "foo" }.

Playground Link: here

Context
I have this type-guarding function that acts as an identity function on the second argument but requires that the first argument is never.

export function unreachable<T>(never: never, result: T): T {
  return result;
}

I then use this to create a throw statement at the end of a switch that will type-error if I expanded the union that went into the switch and neglected to implement a branch that handles the new subtype. This allows me to perform a run-time assertion of an unsupported option and check that I have implemented all supported options in one line.

switch (union.discriminant) {
  case "foo": return doThis();
  case "bar": return doThat();
  default:
    throw Error(unreachable(union, "Unrecognized union type."));

It works beautifully until the union is not not actually a union at all (see case c in the example above). This ruined my ability to keep the switch form plus unreachable generically regardless of whether the switch argument is a discriminated union or just a simple object. The thing is, I run into situations all the time where the switch will first art with one discriminant value and grow later. But at any point in time I want to be able to capture both the run-time error and the compile-time check that all valid options have been exhausted. This includes when there is only one case on the switch.

Therefore I would find it useful that the behavior between c and b above are consistent. That way, I can use the switch boilerplate starting with the non-union and then expand it as I go.

Related Issues:
This seems to be related (but still somewhat different) to #20375 and #30557. I'm fine if this is unified into an existing issue as the essence of this issue remains addressed.

Metadata

Metadata

Assignees

No one assigned

    Labels

    DuplicateAn existing issue was already created

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions