TypeScript: No error when an arrow function uses a variable from an unreachable branch

🔎 Search Terms

variable used before declaration function

🕗 Version & Regression Information

  • This is the behavior in every version I tried since v3.3.3333

⏯ Playground Link

https://www.typescriptlang.org/play?ts=5.3.2#code/BTCUAIF4D5wbwFDnASwGbmAWQIYBcALAOhwCMBnYARgkjvBviWXACcBTARwFd3y8AggDsUAW3woA9kIBirHKPYhasAMbTykgDbsiWyQHNgaSZNCgA3M2Qc83VkKvIAvs3VD+4E5KjgATE7g7po6eobGppYIzqBgVkA

💻 Code

(() => {
  if (Math.abs(1) === 1) {
    requestAnimationFrame(() => console.log(foo));
    return;
  }
  const foo = 2;
  console.log(foo);
})();

🙁 Actual behavior

Fails at runtime with

Uncaught ReferenceError: Cannot access 'foo' before initialization

🙂 Expected behavior

The same error, but at compile time

Additional information about the issue

If you replace requestAnimationFrame() with an IIFE (``), it correct

About this issue

  • Original URL
  • State: closed
  • Created 6 months ago
  • Comments: 24 (2 by maintainers)

Most upvoted comments

This is a duplicate of #11498. It’s sometimes valid and idiomatic to write this code, but we don’t know if it’s safe or not unless we know the semantics of the receiver of the callback. It’s not practical to always error on this, so the only thing we can do is never error.

Yes, I’m fully aware - I was specifically arguing against Ryan’s assertion that this code could ever be valid as written (it can’t).

No, that’s irrelevant - even if the branch condition is only sometimes true, calling requestAnimationFrame always corresponds to an early return.

  • If the branch is not called, foo is initialized.

  • If the branch is called, foo is never initialized, and the CFA could reliably conclude that accessing foo is an error - IN THIS CASE.

That’s seems true. However, because of #11498, for the general case, const declarations are hoisted. So #11489 is the crux here. By hoisting, the CFA can safely assume reference by (async) functions at the top scope and any subscopes, early returns or no. To allow for this case, it would have to know otherwise.

No, that’s irrelevant - even if the branch condition is only sometimes true, calling requestAnimationFrame always corresponds to an early return.

@RyanCavanaugh It is true in this case that, if requestAnimationFrame is called, then the const will never be initialized (because the function returns early)—regardless of how many frames it takes to call the callback or the exact condition in the if.

Not in this case; because the if() block always terminates, this can never be valid (no matter when the callback runs, the variable can never have been declared.

This isn’t a useful hypothetical. If it “can never be valid” then no one would write the if in the first place. If Math.abs(1) were -1 and requestAnimationFrame waited until the next event loop tick to invoke its argument, then the code would be valid. The point is that TypeScript doesn’t know either of those facts and thus can’t conclude that it’s invalid.