Skip to content

Type guard incorrectly erases union type  #54143

Closed
@rf-figma

Description

@rf-figma

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.

Metadata

Metadata

Assignees

Labels

BugA bug in TypeScriptFix AvailableA PR has been opened for this issue

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions