Repository navigation
Create flag strictVariableInitialization #60064
Description
🔍 Search Terms
uninitialized variable, undefined, closure, strict property initialization,
✅ Viability Checklist
- This wouldn't be a breaking change in existing TypeScript/JavaScript code
- This wouldn't change the runtime behavior of existing JavaScript code
- This could be implemented without emitting different JS based on the types of the expressions
- This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
- This isn't a request to add a new utility type: https://github.com/microsoft/TypeScript/wiki/No-New-Utility-Types
- This feature would agree with the rest of our Design Goals: https://github.com/Microsoft/TypeScript/wiki/TypeScript-Design-Goals
⭐ Suggestion
I would like to see a check that ensures that no variable whose type is not permitted to be undefined may remain uninitialized at the end of its scope.
📃 Motivating Example
TypeScript allows unsafety that can and does catch users by surprise when using variables in a closure. Normally, TS won't let you access an uninitialized variable:
function doSomething() {
let foo: string;
foo.toLowerCase(); // TS ERROR: Variable 'foo' is used before being assigned
}However, TypeScript optimistically assumes that variables are initialized when used in closures.
let foo: string;
function printFoo() {
console.log(foo.toLowerCase());
}
printFoo(); // Uncaught TypeError: Cannot read properties of undefined (reading 'toLowerCase')That's for good reason, but sometimes, as in the above case, this is provably unsafe, since foo is guaranteed not to be initialized.
The new flag "strictVariableInitialization" ensures that a variable must be initialized by the end of all reachable codepaths in its scope.
let foo: string; // (proposed) TS ERROR: `foo` is not initialized in all reachable codepaths
function printFoo() {
console.log(foo.toLowerCase());
}
printFoo()Of course, variables whose type includes undefined are still permitted to be uninitialized.
let foo: string | undefined;
function printFoo() {
console.log(foo?.toLowerCase());
}
printFoo();This check is highly analogous to strictPropertyInitialization for classes.
💻 Use Cases
See typescript-eslint/typescript-eslint#9565 for a somewhat-wordier proposal in typescript-eslint, and typescript-eslint/typescript-eslint#4513 and typescript-eslint/typescript-eslint#10055 (comment) for cases where this has caused confusion in the wild.
In short, people who have written code that does check for initialization of non-nullable variables become confused by the linter informing them that the check is unnecessary according to the types, even though they can see that it is necessary at runtime:
let foo: Something
function useFoo() {
if (foo != null) { // linter flags this condition as unnecessary since foo cannot be nullish
foo.bar();
}
}The code should be rewritten as
let foo: Something | undefined
function useFoo() {
if (foo != null) {
foo.bar();
}
}
Activity
RyanCavanaugh commentedon Sep 25, 2024
This is already implemented (unflagged) in nightly. Playground
kirkwaiblinger commentedon Sep 26, 2024
Oh! I should have checked that 😆
Playing around with it, it looks like it only reports a variable that's never assigned, is that right? I'd love to see something like this flagged too (playground):
But I guess I'm probably too late to the party to bring that up. Do you know the issue/PR offhand where this was discussed?
kirkwaiblinger commentedon Sep 26, 2024
Ok, I'm finding #55887, #23305.
I guess I still think there is value in having TS check that every reachable codepath is assigned, not just at least one (just like you would require in order to actually use the variable directly, rather than in a closure), and I'm not seeing that discussed explicitly in the PR or issues.
Is that a change you'd be open to? Or is this too late to revisit?
RyanCavanaugh commentedon Sep 26, 2024
"every" implies a level of analysis that's not really possible. Like if you had
if it turned out that
someothercalledreadFoo, you'd have a problem here, even though "all codepaths" initializefoo. The rule you really need is to always initializelets, which can be accomplished with a lint rule.It's not super high priority to move where in the gray zone the detected/not-detected line is.
kirkwaiblinger commentedon Sep 27, 2024
Completely agree and I agree this is out of scope to solve. Note that this exists with
strictPropertyInitializationtooWell... temporarily leaving non-nullable
lets uninitialized is valuable, though. Likeor doing things where you want to trigger "evolving
any" behavior.it's a bummer to lose some of those features 🤷♂️ (see also typescript-eslint/typescript-eslint#9565 (comment)).
completely fair! And no hard feelings if you decide to close this issue!
My main point is just - it would help me as a user understand the feature better if it follows basically identical standards as set by
strictPropertyInitialization.So, if SPI errors here
so should the new checks on
letsI'd agree that the "definitely not assigned" standard is the only definitely unambiguous check here. I just wish that we could set the detected/undetected line in the grey zone to the exact same position as SPI for the sake of user comprehension.