Please help me to understand how TS narrow types. I have a simple foo function which has forEach iterator over an arbitrary array. It's clear that console.log will log false after executing this code, but TS insists that it's true, which is wrong.
My expectation is if TS can't handle forEach or similar functions because of possible asynchronity, then it should suggest boolean. This situation really frustrates me and slows my productivity, because I need to re-check everything 10 times to prove that TS is wrong and my code is correct.
function foo() {
let canActivate = true;
['foo'].forEach(() => {
canActivate = false;
})
console.log(canActivate);
}
foo();

This excellent blog post explains that this is a feature, not a bug. I'll quote a big chunk here (emphasis added):
Example
let a: number | null = 42
makeSideEffect()
a // is `a` still a number?
function makeSideEffect() {
// omitted...
}
...
One might ask [the] compiler to infer what
makeSideEffectdoes since we can provide the source of the function. However this is not practically feasible because of ambient function and (possibly polymorphic) recursion. Compiler will be trapped in infinite loops if we instruct it to infer arbitrary deep functions, as halting problem per se.So a realistic compiler must guess what a function does by a consistent strategy. Naturally we have two alternatives:
- Assume every function does not have relevant side effect: e.g. assignment like
a = null. We call this optimistic.- Assume every function does have side effect. We call this strategy pessimistic.
Spoiler: TypeScript uses optimistic strategy.
One more short excerpt:
No keyword will tell [the] compiler whether callback function will be called immediately, nor static analysis will tell the behavior of a function:
setTimeoutandforEachis the same in the view of compiler.So the following example will not compile.
var a: string | number = 42 // smart cast to number
someArray.forEach(() => {
a.toFixed() // error, string | number does not have method `toFixed`
})
So there you have it. The post also explains that there is no solution to get TypeScript to recognize the side effects of a forEach function, or any function for that matter other than immediately invoked functions. That means you can either:
forEach, which causes the final canActivate to be inferred correctly as boolean rather than true.map, filter, forEach, etc. That means aim for immutability, no side effects, and so on.All in all, not great news. Hopefully in the future TypeScript will gain some sort of feature that will allow us to warn the compiler when a function is modifying a certain variable—at least to prevent a premature assumption that something which started off as true will always remain so.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With