Itay Ganor
Itay Ganor

Reputation: 4205

Typescript type inference in an If statement doesn't work as a variable

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:

enter image description here

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

Answers (1)

jcalz
jcalz

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 consts and use them later. Your code above will therefore work as desired with no modifications.

Playground link to code


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.


Playground link to code

Upvotes: 2

Related Questions