Skip to content

[5.0.2] Incorrect property type deduction with type guard #53313

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
quangloc99 opened this issue Mar 17, 2023 · 8 comments
Closed

[5.0.2] Incorrect property type deduction with type guard #53313

quangloc99 opened this issue Mar 17, 2023 · 8 comments
Labels
Not a Defect This behavior is one of several equally-correct options

Comments

@quangloc99
Copy link

Bug Report

🔎 Search Terms

type guard, control flow analysis

As this only breaks in version 5.0.2, I only found #53311 related to this issue but it seems like it is not the same.

🕗 Version & Regression Information

  • This changed between versions 4.9.5 and 5.0.2

⏯ Playground Link

Playground link with relevant code

💻 Code

The playground contains a working example and a failed example. The following is the failed one to keep it short.

  type If<Condition extends boolean, TrueType, FalseType = undefined> = Condition extends true
      ? TrueType
      : Condition extends false
      ? FalseType
      : TrueType | FalseType;

  type X<T extends boolean = boolean> = {
    x: If<T, number>;
  }

  function hasDefinedField(o: X): o is X<true> {
    return o.x !== undefined;
  }

  function doStuff(o: X): number {
    if (hasDefinedField(o)) {
      return o.x;
    }
    return 0;
  }

🙁 Actual behavior

  • In side the if statement of doStuff, o.x has the type of number | undefined.

🙂 Expected behavior

  • o.x should be number (as o should be X<true>). The working example shows this should be possible.
@quangloc99
Copy link
Author

There was a comment before about true false branch but it got deleted :(.

Anyway, after that comment, I figure the following works too

  function doStuff(o: X<true> | X<false>): number {
    if (hasDefinedField(o)) {
      return o.x;
    }
    return 0;
  }

But I think X<boolean> should be the same as X<true> | X<false>.

@MartinJohns
Copy link
Contributor

I've deleted the comment because I have no clue why it's been different in 4.9. And X<boolean> is equal to X<true | false>.

@quangloc99
Copy link
Author

Yes, X<boolean> should be X<true | false>, but structurally X<boolean> should be the same as X<true> | X<false> as well.

type If<Condition extends boolean, TrueType, FalseType = undefined> = Condition extends true
    ? TrueType
    : Condition extends false
    ? FalseType
    : TrueType | FalseType;

type X<T extends boolean = boolean> = { x: If<T, number>; }

declare let a: X<boolean>;
declare let b: X<true> | X<false>;

a = b;

This works, because X<boolean> and X<true> | X<false> has the same structure.

@RyanCavanaugh
Copy link
Member

RyanCavanaugh commented Mar 17, 2023

For all X<T>, X<true | false> isn't the same as X<true> | X<false>. Object types do not distribute over unions; it matters which you write here. "Small" unions of literals can be evaluated combinatorially in your example shown, but not always.

@RyanCavanaugh RyanCavanaugh added the Not a Defect This behavior is one of several equally-correct options label Mar 17, 2023
@quangloc99
Copy link
Author

So can you help we with a work around for this? Previously, in 4.9.5, I use type guard to force the type of an object to have defined property. Now I can not do this in 5.0.2.

@quangloc99
Copy link
Author

quangloc99 commented Mar 18, 2023

For all X<T>, X<true | false> isn't the same as X<true> | X<false>. Object types do not distribute over unions; it matters which you write here. "Small" unions of literals can be evaluated combinatorially in your example shown, but not always.

Sorry, but I think we have some minor miscommunication. What I want to point out is not to have a type guard to narrow down the true from the union false | true, but to narrow down X<true> from X<true | false> as an independent type. Sorry again if my interpretation is not clear enough, but the following example might show what I meant:

type A = X<true>;
function hasDefinedField(o: X): o is A {
    return o.x !== undefined;
}

function doStuff(o: X): number {
    if (hasDefinedField(o)) {
        return o.x;
    }
    return 0;
}

Playground link

This is also a working example. What I want here is to narrow X<boolean> to the exact type A, which is essentially the same type as X<true> as an independent type.

@quangloc99
Copy link
Author

Hi @RyanCavanaugh. Sorry for the mention, but I see you labeled this issue Not a Defect, so I think you may have missed the above follow-up question. I am confused because the code in the issue does not work, while the code with the type assignment type A = X<true> still works fine. Please correct me if I'm wrong, but I genuinely think both codes should have the same behavior.

@github-actions
Copy link

github-actions bot commented Jun 8, 2023

This issue has been marked as 'Not a Defect' and has seen no recent activity. It has been automatically closed for house-keeping purposes.

@github-actions github-actions bot closed this as not planned Won't fix, can't repro, duplicate, stale Jun 8, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Not a Defect This behavior is one of several equally-correct options
Projects
None yet
Development

No branches or pull requests

3 participants