Skip to content

Inconsistent property type deduction with type guard. #53453

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 23, 2023 · 7 comments
Closed

Inconsistent property type deduction with type guard. #53453

quangloc99 opened this issue Mar 23, 2023 · 7 comments
Labels
Design Limitation Constraints of the existing architecture prevent this from being fixed

Comments

@quangloc99
Copy link

Bug Report

I created this issue to reopen #53313. It was marked as Not a defect, but there are follow-up questions from me that are unanswered. I still believe both cases described below should have the same behavior.

🔎 Search Terms

type guard, control flow analysis

🕗 Version & Regression Information

  • This changed between versions 4.9.5 and 5.0.2

⏯ Playground Link

Playground link with relevant code

💻 Code

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>;
}

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

  function doStuff(o: X): number {
    if (hasDefinedField(o)) {
      return o.x;  // <--- Failed to deduce the type
    }
    return 0;
  }
}

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

  function doStuff(o: X): number {
    if (hasDefinedField(o)) {
      return o.x;  // <--- Type is now _correct_
    }
    return 0;
  }
}

🙁 Actual behavior

The Failed example failed at deducing its property, while in the Working example, only by aliasing type A = X<true>, the error is gone.

🙂 Expected behavior

Both examples should have the same behavior.

@fatcerberus
Copy link

Since making a type alias changes the behavior, I’m guessing this has something to do with variance measurement.

@MartinJohns
Copy link
Contributor

Possibly a duplicate of #48936 (comment).

@fatcerberus
Copy link

fatcerberus commented Mar 23, 2023

Proof that variance measurement affects how X relates to itself:
Playground

type Xtrue = X<true>;
declare let a: X;
declare let b: X<true>;
declare let c: Xtrue;
b = a;  // ok
c = a;  // error!

TS seems to think that X<boolean> is already assignable to X<true>, so it doesn't bother narrowing. Basically it (incorrectly) sees your type guard as isAnimal(cat)--and deliberately ignores it because (it thinks) that would widen the type.

@RyanCavanaugh RyanCavanaugh added the Needs Investigation This issue needs a team member to investigate its status. label Mar 23, 2023
@ahejlsberg
Copy link
Member

This is ultimately a case of unmeasurable variance that is being measured anyway, which is a known design limitation. Specifically, the compiler measures the Condition type parameter of the If type to be bi-variant, and then by implication measures the T type parameter of X to be bi-variant. It is possible to add an out modifier to T in X to force co-variance, which fixes the issue as reported. However, we don't permit variance annotations on aliased conditional types (mostly because they often have variance that can't be expressed with just in or out), so unfortunately it isn't possible to annotate the If type.

The example works when an alias is injected because we only rely on measured variance when relating instances of the same type alias. In general, 4.9 and 5.0 both exhibit the variance measurement inconsistency as illustrated by @fatcerberus here.

The reason the example changed between 4.9 and 5.0 is because narrowing by user defined type predicates (such as hasDefinedField) now picks the asserted type only when it is a subtype of the argument type--and here, according to variance measurement, X<boolean> and X<true> are mutual subtypes.

@ahejlsberg ahejlsberg added Design Limitation Constraints of the existing architecture prevent this from being fixed and removed Needs Investigation This issue needs a team member to investigate its status. labels Mar 25, 2023
@fatcerberus
Copy link

fatcerberus commented Mar 25, 2023

now picks the asserted type only when it is a subtype of the argument type--and here, according to variance measurement, X<boolean> and X<true> are mutual subtypes.

Doesn’t that imply it should pick the asserted type then?

@ahejlsberg
Copy link
Member

Let me rephrase that... We stay with the original type if it is a subtype of the asserted type; otherwise, we narrow to the asserted type if it is a subtype of the original type; otherwise, if the types aren't related at all, we produce an intersection of the types.

@quangloc99
Copy link
Author

Thanks for the responses. I still want to ask the same question. If this is a known limitation, is there a known workaround (besides the working example)?

@ahejlsberg ahejlsberg removed their assignment May 22, 2023
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

5 participants