Description
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.