Reputation: 534
I modified the flag
variable but TS didn't find it. Is this a bug?
function fn () {
let flag = true
function f () {
// modify flag
flag = false
}
for (let i = 0; i < 10; i++) {
// error: This condition will always return 'false' since the types 'true' and 'false' have no overlap.
if (flag === false) { break }
f()
}
return flag
};
fn()
Upvotes: 4
Views: 407
Reputation: 328788
See microsoft/TypeScript#9998 for an authoritative answer to this question.
The behavior you're seeing is not a bug, but a design limitation in TypeScript which really can't be addressed very easily. You may have noticed that this version of fn()
works fine:
function fn() {
let flag = true
for (let i = 0; i < 10; i++) {
if (flag === false) { break }
flag = false;
}
return flag
};
Here the compiler infers that flag
is of type boolean
, which in TS is a shorthand equivalent of the union true | false
. When you assign true
to it upon initialization, the compiler narrows the apparent type of flag
from true | false
to just true
. After this line, the compiler assumes that flag
is definitely true. Inside the for
loop scope, the compiler sees another assignment flag = false
and knows that it is possible to enter the loop body with flag
being either true
or false
, and so the narrowing to true
is essentially reset to boolean
inside the loop. This general behavior is called control flow analysis, where the compiler analyzes the order in which code is evaluated so it can give more specific types to certain values in certain scopes.
Unfortunately, as you note, this breaks when you refactor that flag = false
line into the body of f()
. The compiler does not reset the narrowing after f()
is called, and the apparent type of flag
is still true
in the entire loop. And therefore it erroneously complains that the check flag === false
is useless because it thinks it will always return false
.
But why does the compiler not realize that calling f()
has an effect on the closed-over flag
variable? The answer is that you just can't perform control flow analysis with closure mutations in a way which is both accurate and performant. You can't perform inlining for every function call, re-analyzing the implementation of every function for each call to it. This "wouldn't be even remotely practical", according to the TS team dev lead (in ms/TS#9998, linked above). Possibly it could be done for a toy example like in this question, but for any real world code base you would end up having prohibitively long compile times... maybe even impossibly long, since control flow analysis through function calls is almost like simulating the program being run for every possible set of inputs.
So accuracy is not feasible. Instead, the compiler must make optimizing assumptions, which will turn out to be incorrect in some cases. TypeScript currently makes the "optimistic" assumption that function calls have no effect on state. This behaves well in a lot of circumstances, and it certainly saves compilation time. But if function calls do affect state, then bad things happen, as you see here
The opposite, "pessimistic" assumption is that function calls may affect all state, and that every function call should reset all narrowings. This would be much worse in most situations, since harmless function calls are incredibly more common than state-changing ones.
It is possible that something better could be done here, but for now, this is the way it is. TypeScript just isn't equipped to track state mutations inside of function calls.
So that's the answer to the question as asked.
But if you want to know how to prevent this erroneous error and you can't easily refactor to avoid state-changing functions, the easiest way is to prevent control flow narrowing of flag
entirely. You can do this by changing the initial assignment of flag
so that the compiler does not realize that it's a literal true
value:
let flag = true as boolean;
By writing true as boolean
you are widening the type of the true
value to true | false
, and the compiler now has no idea what the initial state of flag
is. Therefore at every point in the rest of the scope, flag
is either true
or false
according to the compiler. And so checking flag === false
is accepted.
Upvotes: 6