I have encountered an issue where TypeScript or JavaScript does not throw an error or warning for using an uninitialized variable within code snippets. Consider the following scenarios:
In the first code snippet: typescript Copy code
const a = [12].map(val => b.includes(val));
const b = [12].filter(Boolean);
In the second code snippet: typescript Copy code
const a = [].map(val => b.includes(val));
const b = [] as number[];
In runtime, it's evident that these code snippets will not execute due to b being uninitialized. However, during the TypeScript/JavaScript build process and even while running default tests in Sonar Cloud, there are no warnings or errors generated.
I'm curious to understand why TypeScript/Javascript does not issue warnings or errors in such cases, and I'm seeking guidance on how to modify my code or configuration to enforce stricter checks to catch uninitialized variable usage during the build process or at runtime.
How can I configure TypeScript or use best practices to ensure that such code, where variables are used before being initialized, either fails the build or triggers an error during development or execution?
This is a missing feature of TypeScript and there's currently no way to deal with it without using external tools like linters.
There is a longstanding open feature request at microsoft/TypeScript#23305 to catch closed-over uninitialized variable usages, and even a recent-ish pull request at microsoft/TypeScript#55887 (as of 2023-11-27 when I wrote this answer) which would address some of it, and a discussion of the feature in a TS team design meeting documented at microsoft/TypeScript#55993. Unfortunately this pull request would fail to catch the error here, and even if this particular case were resolved, it is almost certain that TypeScript will never be able to consistently catch such problems in general.
The wider problem here is that it is computationally too expensive for TypeScript to accurately track control flow through function calls, which is especially noticeable when dealing with variables in closures. See microsoft/TypeScript#9998 for a detailed discussion about the issue. So, instead of doing it accurately, TypeScript makes various simplifying assumptions, which work well enough in a wide variety of real-world scenarios, but have some serious drawbacks too.
In this case the simplification is that closed-over variables are optimistically presumed to be initialized. The alternative simplification, that any closed-over variable that can possibly be accessed before its initialization is pessimistically assumed to be uninitialized would fix this problem, but cause many false errors in perfectly safe real-world code. Again, the correct behavior, to track control flow across function boundaries, would be prohibitively expensive to perform, as it would require a potentially amount of analysis for every function.
The pull request at microsoft/TypeScript#55993 catches situations where a variable is never assigned, like
let b: number[]; // b never set anywhere in entire code base
const a = [12].map(val => b.includes(val));
which is easy enough to analyze, but wouldn't have a problem with your code. Maybe someone could implement a check that determines if a variable is not initialized before its first reference lexically, which would catch things like your
const a = [12].map(val => b.includes(val));
const b = [12]; // oops too late
but it would also complain about things like
const f = (x: number): number => x > 1 ? g(x - 1) : 1; // g doesn't exist yet?
const g = (x: number): number => x > 1 ? f(x - 2) : 0;
f(10);
which would be a breaking change for real-world code, so they're unlikely to introduce that into TypeScript itself. Breaking changes have to really pay for themselves, and for many users, the cure would be worse than the disease. So we're kind of stuck; this is probably not going to be addressed by pure TypeScript.
If you would prefer to see such false positives instead of false negatives, you could possibly enable a linter rule like ESLint's no-use-before-define, but again, it's not a TypeScript feature.
Playground link to code
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