Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Never-initialized variable type is not narrowed to undefined #61496

Open
jtbandes opened this issue Mar 27, 2025 · 4 comments
Open

Never-initialized variable type is not narrowed to undefined #61496

jtbandes opened this issue Mar 27, 2025 · 4 comments
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript

Comments

@jtbandes
Copy link
Contributor

πŸ”Ž Search Terms

narrow uninitialized, uninitialized undefined

πŸ•— Version & Regression Information

  • This is the behavior in every version I tried, and I reviewed the FAQ for entries about initialized/uninitialized

⏯ Playground Link

https://www.typescriptlang.org/play/?ts=5.9.0-dev.20250326#code/MYewdgzgLgBAZiEMC8MAUUDuIBcNoBOAlmAOYCUeAbiEQCYoB8MA3gFAwwBEArhAKb4oxYFC4BuDjCkB6GTH4QANiSgBaOkQgBDAEZL+asPwAe6lcZgABKAE8ADouDF76xRagySRdXX7AlbQJtKCJwCCkDWHB+PEISUhgAHxgeMD84En46SSkiOHQYlGRULBByVilOORgAPQB+KpgagDlEeykAXzZutgQQNC4ucnEgA

πŸ’» Code

const foo = (two: string): void => {
  "use strict";
  
  // eslint-disable-next-line @typescript-eslint/init-declarations
  let one: string | undefined;

  if (one === two) {
    // ^?  let one: string | undefined
    // Noop
  }
}

foo("");

πŸ™ Actual behavior

The type is not narrowed, which prevents typescript-eslint from flagging this comparison that always fails: typescript-eslint playground

πŸ™‚ Expected behavior

The one === two comparison should be flagged because it always returns false (there is no way one has any value other than undefined).

Additional information about the issue

Also filed a related issue with ESLint: eslint/eslint#19581

@RyanCavanaugh
Copy link
Member

You're always allowed to compare to undefined, see #11920

I believe not narrowing here is a concession to #9998 to reduce false positives when assignments occur outside TS's immediate CFA

@jtbandes
Copy link
Contributor Author

You're always allowed to compare to undefined, see #11920

That makes sense – I guess I would expect this part to be flagged by typescript-eslint rather than TS itself, however due to the lack of narrowing it is not flagged.

@kirkwaiblinger
Copy link

kirkwaiblinger commented Mar 28, 2025

Noting that this does have observable impact within TS, too, though, not just for tooling, for example:

let alwaysUndefined: string | undefined;
let foo = alwaysUndefined?.toLocaleString(); // allowed (but bad code)
export {}
let alwaysUndefined: string | undefined = undefined;
let foo = alwaysUndefined?.toLocaleString(); // TS ERROR
export {}

@kirkwaiblinger
Copy link

When the let variable is captured in closures and reassigned the existing behavior definitely seems reasonable, but I'd think in theory TS could safely analyze the never-captured, never-reassigned variable as simply undefined. Might have some confusing/negative repercussions for user experience if adding closure let reassignments can alter typechecking at a distance though.

@RyanCavanaugh RyanCavanaugh added Suggestion An idea for TypeScript Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature labels Mar 28, 2025
jtbandes added a commit to foxglove/eslint-plugin that referenced this issue Apr 1, 2025
### Changelog
Added `@foxglove/no-never-initialized-let` (enabled by default).

### Docs

Added to readme

### Description

Adds a rule to fill a gap when `init-declarations` is not enabled.
Related discussions: eslint/eslint#19581 &
microsoft/TypeScript#61496

Copying from readme:

> ###
[`@foxglove/no-never-initialized-let`](./no-never-initialized-let.js)
> 
> Disallow variable declarations that use `let` but have no intitial
value and are never assigned. These variables will always be `undefined`
and are likely a programmer error.
> 
> The builtin
[prefer-const](https://eslint.org/docs/latest/rules/prefer-const) rule
doesn't flag these because they lack an initializer. Otherwise, they
could be flagged by
[init-declarations](https://eslint.org/docs/latest/rules/init-declarations),
but this rule is mostly stylistic and has some implications for
TypeScript type inference & refinement. (See
[eslint/eslint#19581](eslint/eslint#19581) &
[microsoft/TypeScript#61496](microsoft/TypeScript#61496)
for more discussion.)
> 
> Examples of **incorrect** code for this rule:
> 
> ```ts
> let prevX;
> let prevY;
> if (x !== prevX) {
>   prevX = x;
> }
> if (y !== prevY) {
>   prevX = x; // typo, should have been Y
> }
> ```
> 
> Examples of **correct** code for this rule:
> 
> ```ts
> let prevX;
> let prevY;
> if (x !== prevX) {
>   prevX = x;
> }
> if (y !== prevY) {
>   prevY = y;
> }
> ```
> 
>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

3 participants