Reputation: 4205
Consider the following example:
class A {}
class B extends A {
foo() {
return 6;
}
}
const variable: A = new B();
const isValid = variable instanceof B;
if (isValid) {
variable.foo();
}
The .foo()
call prompts the following error:
It makes sense because variable
is of type A
. But variable.foo()
will only run if variable
is an instance of B
.
The issue does not happen when doing it like this:
class A {}
class B extends A {
foo() {
return 6;
}
}
const variable: A = new B();
if (variable instanceof B) {
variable.foo();
}
Why does it matter if the condition is stored in a variable, rather than written explicitly within the if
?
Upvotes: 5
Views: 1634
Reputation: 327954
Update for TS4.4:
TypeScript 4.4 will introduce support for control flow analysis of aliased conditions. This means you can sometimes save the results of type guards into const
s and use them later. Your code above will therefore work as desired with no modifications.
Pre TS4.4 answer:
There is a suggestion at microsoft/TypeScript#12184 (among others, see that issue for links to them) to allow storing the results of a type guard into a boolean variable for later use. While most people agree it would be nice to have this, the lead architect for the language said:
This would require us to track what effects a particular value for one variable implies for other variables, which would add a good deal of complexity (and associated performance penalty) to the control flow analyzer. Still, we'll keep it as a suggestion.
To expand on that a little bit, currently the compiler recognizes that, inside a code block where variable instanceof B
has been tested and evaluated to true
, the type of variable
can be narrowed to B
... but outside of that scope, the compiler can just "forget" about such narrowing:
if (variable instanceof B) {
variable.foo(); // okay
}
variable.foo(); // error
In order for something like the following to work:
const isValid = variable instanceof B;
if (isValid) {
variable.foo();
}
the compiler would not be allowed to "forget" the narrowing until isValid
itself... or anything that depends on isValid
goes out of scope. This might seem reasonable on the surface, but for every case like this where you want such memory, it seems like there would be many cases where the extra work would be unnecessary. For example, if you had this:
const bar = Math.random() < 0.5 ? { a: 123, b: 456 } : undefined;
const baz = bar?.a;
Should the compiler "remember" that baz
will be a number
if and only if bar
is an {a: number, b: number}
? Perhaps... but unless sometime later someone actually uses this fact, as in:
const qux = typeof baz === "number" ? bar.b : 789;
then keeping track of that is just wasted effort.
It's possible that in the future someone will devise a way to do this that's better than the current situation without making control flow analysis prohibitively expensive. Maybe someone who wants such behavior on a boolean
variable could manually annotate it with something like a type predicate, as mentioned in this comment on a related issue?
const isValid: variable is B = variable instanceof B; // not valid syntax now
But for now, it's not part of the language. If you really feel strongly about this you might want to go to the relevant GitHub issues and give them your 👍 or describe your use case and explain why the current behavior of using non-stored type guards is insufficient. If enough people do this it raises the chances that something will eventually be implemented. I wouldn't count on it, though.
For the foreseeable future, you will be better off just using your type guards immediately instead of trying to save them for later. One way to get closer to this "delayed" behavior is to refactor so that what you are saving is more functional than a boolean
:
const varIfValid = variable instanceof B ? variable : undefined;
if (varIfValid) {
varIfValid.foo(); // okay
}
That works because varIfValid
is B | undefined
, a union more directly related to calling foo()
than true | false
is.
Upvotes: 2