Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

TypeScript Type Narrowing Error with forEach

Tags:

typescript

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();

ts playground

like image 824
Eugene Karataev Avatar asked Jun 29 '26 00:06

Eugene Karataev


1 Answers

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 makeSideEffect does 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: setTimeout and forEach is 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:

  • Use a regular for loop instead of forEach, which causes the final canActivate to be inferred correctly as boolean rather than true.
  • Use immediately invoked functions when possible. Or...
  • Stick to functional programming (FP) paradigms when using FP-style features like 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.

like image 127
jdaz Avatar answered Jul 01 '26 15:07

jdaz



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!