Skip to content

Tagged type narrowing doesn't work when one of the tags is a union type #43026

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
andriy-kudrya opened this issue Mar 1, 2021 · 5 comments
Closed
Labels
Design Limitation Constraints of the existing architecture prevent this from being fixed

Comments

@andriy-kudrya
Copy link

Bug Report

Tagged type narrowing doesn't work when one of the tags is a union type. Sorry, not sure how to be more descriptive

🔎 Search Terms

union narrowing

🕗 Version & Regression Information

3.9.7, 4.2.2, Nightly

  • This is a crash
  • This changed between versions ______ and _______
  • This is the behavior in every version I tried, and I reviewed the FAQ for entries about common bugs that aren't bugs
  • I was unable to test this on prior versions because _______

⏯ Playground Link

Playground link with relevant code

💻 Code

interface A {
    type: 'a'
}

interface BC {
    type: 'b' | 'c'
}

declare const value: A | BC

let a: A

if (value.type === 'a')
    a = value

if (value.type !== 'b' && value.type !== 'c')
    // type narrowing doesn't work here, value has type "A | BC" instead of "A"
    a = value

declare function isA(x: 'a' | 'b' | 'c'): x is 'a'

if (isA(value.type))
    // type narrowing doesn't work with type guard either
    a = value

🙁 Actual behavior

Type of a is not narrowed to A

🙂 Expected behavior

Type of a is narrowed to A

@koushikkothagal
Copy link

I have a simpler use case of another instance where type narrowing doesn't work.

The union type doesn't narrow unless the expression being checked in the if statement is exactly the variable itself. Any redirections in terms of variable assignments causes the type narrowing to fail.

let a : string | undefined;


if (new Date().getHours() > 4) {
    a = "value";
}

let b = !!a;

if (b) {
    a; // should be string, but is still union type
}

if (!!a) {
    a; // type is narrowed to a string here.
}

TypeScript Playground link: HERE

@MartinJohns
Copy link
Contributor

@koushikkothagal That would be #12184 (when using const instead of let).

@RyanCavanaugh RyanCavanaugh added the Design Limitation Constraints of the existing architecture prevent this from being fixed label Mar 2, 2021
@RyanCavanaugh
Copy link
Member

The core problem is more easily explained in the expanded form

if (value.type !== 'b') {
  // ~~ here ~~
  if (value.type !== 'c') { 

At this point, there's no type we can narrow value to - it's not A yet because there's still the c case. So when the subsequent c narrowing happens, we're still just narrowing A | BC, which again does nothing. We'd need to give it the interim type (A | BC) & Not<{value: "b"}>, but we don't have any way to represent this without negated types.

@imdavidmin
Copy link

If you add value.type in the if condition

if (value.type !== 'b' && value.type !== 'c')
    // type narrowing doesn't work here, value has type "A | BC" instead of "A"
    value.type
    a = value

Seems that in IDE Typescript is able to figure out the only value.type value here is a, if you mouse hover over value.type. I get @RyanCavanaugh's point, but surely Typescript should be able to infer the type here? Is there any relevant feature suggestion put in to resolve this issue?

@danny-does-stuff
Copy link

danny-does-stuff commented May 15, 2024

I made a TS playground showing what is IMO an even more simple case. I also made a Flow try showing the exact same behavior, but with no errors

type ABCDBroken = 
  | { status: 'A' | 'B'; extra: string }
  | { status: 'C' | 'D' }

type ABCDWorking =
  | { status: 'A' | 'B'; extra: string }
  | { status: 'C' }
  | { status: 'D' }

function testItOut(abcd: ABCDBroken) { // Replace `ABCDBroken` with `ABCDWorking` and see the problem magically resolve!
  if (abcd.status === 'C' || abcd.status === 'D') {
    return
  }
  let s: 'A' | 'B' = abcd.status // TS knows that `status` is narrowed to 'A' | 'B' ...
  return abcd.extra // ... but it doesn't know that `extra` is available
}

The playground: https://www.typescriptlang.org/play/?#code/C4TwDgpgBAggQgYQCJwE4HsDWEB2UC8UAsAFBRQA+UA3lAM7ACGwArnQFxQDkMXl3cLgG4oEAB7BUjTg1QBLHAHMoAX1LkqtBszacuCPlS5I+akqVCRYiJAHV0qTAuX51-LU1Yduvfl0Ei4pLS9JLOqm6aoTre+qaRNNFeeiYR5iQAZiw4AMbAcuh4wBAMAJLAAPIswAAUjABGOQAmnPDIaFi4AJSJAPS9UABKEGAANow50AAGbSgY2DhTUADucsAAFlAzNvaOzkuMOE30ENAb0GAY9aMQALZQt4yKcjmMo6MgUKgl6KMAbhAAIRuOQZKB1RpNAB02i8BHwhDilCoDWaMM8bHhiJMPWobnI31YqBwbjM5BuwHoel8RkEBCgqOhsMx-SgABUAMpQTA4dDLOhQDbMLbMuhLOQCnCMVAYZYQY7AdA+QwCPhQ9VuQksYkMyFQoJSKCs9VQqD1apQNZQJroEo4LiUnl8wXrYVTA2McUCxh-RhycbXCCkFRAA

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Design Limitation Constraints of the existing architecture prevent this from being fixed
Projects
None yet
Development

No branches or pull requests

6 participants