Skip to content
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

Type guard incorrectly erases union type #54143

Closed
rf-figma opened this issue May 5, 2023 · 2 comments · Fixed by #54169
Closed

Type guard incorrectly erases union type #54143

rf-figma opened this issue May 5, 2023 · 2 comments · Fixed by #54169
Assignees
Labels
Bug A bug in TypeScript Fix Available A PR has been opened for this issue

Comments

@rf-figma
Copy link

rf-figma commented May 5, 2023

Bug Report

I'm seeing surprising behavior when combining type guard methods (using the this is ... type predicate construct) with union types. It looks like if one branch of the union returns a predicate from a method while the other returns a boolean from a method with the same name, calling the method narrows the return type according to the predicate and loses the type information from the other branch.

🔎 Search Terms

predicate, guard, union, narrowing — there's a million results, particularly bugs related to #49625, but I wasn't able to find anything that looks closely connected and hasn't been fixed already.

This also seems kind of related to #50044, but I assume it's different since here the two types aren't subtypes of each other? Adding extra fields to differentiate the two types more also doesn't change the observed behavior at all.

🕗 Version & Regression Information

  • This is the behavior in every version I tried, and I reviewed the FAQ for entries about type guards/predicates

⏯ Playground Link

Playground link with relevant code

💻 Code

type HasAttribute<T> = T & { attribute: number };

class Type1 {
  attribute: number | null = null;

  predicate(): this is HasAttribute<Type1> {
    return true;
  }
}

class Type2 {
  attribute: number | null = null;

  predicate(): boolean {
    return true;
  }
}

function assertType<T>(_val: T) {
}

declare const val: Type1 | Type2;

if (val.predicate()) {
  assertType<number>(val.attribute);
}

🙁 Actual behavior

val.attribute is number, because val is narrowed to HasAttribute<Type1> by the call to val.predicate().

Interestingly, if you change the definition in Type2 to predicate(): this is Type2, the type of val ends up as Type2 | Type1 | HasAttribute<Type1>.

🙂 Expected behavior

val.attribute is number | null, because val is narrowed to HasAttribute<Type1> | Type2 by the call to val.predicate(). The fact that predicate() returns this is HasAttribute<Type1> when called on Type1 shouldn't affect the Type2 branch.

@RyanCavanaugh RyanCavanaugh added the Needs Investigation This issue needs a team member to investigate its status. label May 5, 2023
@ahejlsberg ahejlsberg added Bug A bug in TypeScript and removed Needs Investigation This issue needs a team member to investigate its status. labels May 7, 2023
@ahejlsberg ahejlsberg added this to the TypeScript 5.1.1 milestone May 7, 2023
@ahejlsberg
Copy link
Member

Yeah, our logic that constructs composite type predicates for union types isn't quite right. It simply ignores non-predicate function types. It should instead require that all union constituents have matching type predicates--and if not, the outcome shouldn't be considered a type predicate.

@Ambr33
Copy link

Ambr33 commented Jul 25, 2023

Very nice

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Bug A bug in TypeScript Fix Available A PR has been opened for this issue
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants