Skip to content

[no-unnecessary-condition] False error on uninitialized variable #4513

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

Closed
3 tasks done
Retro64 opened this issue Feb 4, 2022 · 16 comments · Fixed by #11081
Closed
3 tasks done

[no-unnecessary-condition] False error on uninitialized variable #4513

Retro64 opened this issue Feb 4, 2022 · 16 comments · Fixed by #11081
Labels
accepting prs Go ahead, send a pull request that resolves this issue bug Something isn't working locked due to age Please open a new issue if you'd like to say more. See https://typescript-eslint.io/contributing. package: eslint-plugin Issues related to @typescript-eslint/eslint-plugin

Comments

@Retro64
Copy link

Retro64 commented Feb 4, 2022

  • I have tried restarting my IDE and the issue persists.
  • I have updated to the latest version of the packages.
  • I have read the FAQ and my problem is not listed.

Playground

{
  "rules": {
      "@typescript-eslint/no-unnecessary-condition": [
      "error"
    ],
  }
}
let foo: string;

const setFoo = (): void => {
  if (foo?.length < 1) {
    foo = 'foo';
  }
};

tsconfig

{
  "compilerOptions": {
    "outDir": "./lib",
    "module": "commonjs",
    "target": "es2015",
    "lib": ["es6", "es7"],
    "sourceMap": true,
    "allowJs": false,
    "moduleResolution": "node",
    "forceConsistentCasingInFileNames": true,
    "noImplicitReturns": true,
    "noImplicitThis": true,
    "noImplicitAny": true,
    "strict": true,
    "strictNullChecks": true,
    "suppressImplicitAnyIndexErrors": true,
    "noUnusedLocals": true,
    "noErrorTruncation": true,
    "pretty": true,
    "declaration": true
  },
  "include": [
    "src/**/*"
  ]
}

Expected Result

Existence check on a possibly uninitialized variable should not trigger no-unnecessary-condition

For sure, the type might be extended to let foo = string | undefined; as workaround, but that actually looks a bit uncommon for a let definition, where not initializing might be intended.

Actual Result
Unnecessary optional chain on a non-nullish value @typescript-eslint/no-unnecessary-condition

Versions

package version
@typescript-eslint/eslint-plugin 5.10.0
@typescript-eslint/parser 5.10.0
TypeScript 4.5.5
ESLint 8.7.0
ts-node 10.4.0
@Retro64 Retro64 added package: eslint-plugin Issues related to @typescript-eslint/eslint-plugin triage Waiting for team members to take a look labels Feb 4, 2022
@bradzacher
Copy link
Member

The rule was written before optional chaining, and this "use before assigned" case was never added.

It's very, very rare for people to write "use before assigned" variables, and even rarer that people use optional chaining with them.

@bradzacher bradzacher added accepting prs Go ahead, send a pull request that resolves this issue bug Something isn't working and removed triage Waiting for team members to take a look labels Feb 4, 2022
@sosoba

This comment was marked as off-topic.

@bradzacher

This comment was marked as off-topic.

@armano2
Copy link
Collaborator

armano2 commented Jun 1, 2022

this has to be fixed in typescript, uninitialized variables should be reported by typechecker as X | undefined not X or we should be able to receive information that variable is uninitialized in some way

https://www.typescriptlang.org/play?ssl=8&ssc=1&pln=1&pc=1#code/DYUwLgBGIM5gXBOAnAlgOwOYG4BQuAzAV3QGMxUB7dCADwgAoBKCAb1wk4mXCORuhwAdKCxgAFngC++WszxA

let test: string;
function x () {
    return test.length;
}
x();

this code is going to compile without error but, its actually going to produce runtime error

@bradzacher
Copy link
Member

bradzacher commented Jun 2, 2022

We can use scope analysis to handle this. Simply put we can find the declaration, and if it's a variable with no intiailiser - then we can ignore the check.

We already have similar code in the no-unnecessary-type-assertion lint rule:

/**
* Returns true if there's a chance the variable has been used before a value has been assigned to it
*/
function isPossiblyUsedBeforeAssigned(node: ts.Expression): boolean {
const declaration = util.getDeclaration(checker, node);
if (!declaration) {
// don't know what the declaration is for some reason, so just assume the worst
return true;
}
if (
// non-strict mode doesn't care about used before assigned errors
isStrictCompilerOptionEnabled(compilerOptions, 'strictNullChecks') &&
// ignore class properties as they are compile time guarded
// also ignore function arguments as they can't be used before defined
isVariableDeclaration(declaration) &&
// is it `const x!: number`
declaration.initializer === undefined &&
declaration.exclamationToken === undefined &&
declaration.type !== undefined
) {
// check if the defined variable type has changed since assignment
const declarationType = checker.getTypeFromTypeNode(declaration.type);
const type = util.getConstrainedTypeAtLocation(checker, node);
if (declarationType === type) {
// possibly used before assigned, so just skip it
// better to false negative and skip it, than false positive and fix to compile erroring code
//
// no better way to figure this out right now
// https://github.com/Microsoft/TypeScript/issues/31124
return true;
}
}
return false;
}

Relevant TS issue: microsoft/TypeScript#20221

@armano2
Copy link
Collaborator

armano2 commented Jun 2, 2022

that's good idea to use scopes for that

there is only one issue with external variables, eg. exported from another file, but that should be even rarer case

@bradzacher
Copy link
Member

Across module boundaries there be Dragons.
So I'm more than happy for us to fail the case!

@JoshuaKGoldberg
Copy link
Member

JoshuaKGoldberg commented Jun 3, 2024

Note from #9206 that var is also a relevant case:

if (Math.random()) {
  var x = 1;
}
if (x) {}

@kirkwaiblinger
Copy link
Member

kirkwaiblinger commented Jun 3, 2024

I think the rule's behavior is correct in this case. And I think that let x: T; should not be treated as T | undefined for the purposes of this rule, because it is not treated that way by TS.

IMO if you plan to read a variable in its uninitialized state, you should use let x: T | undefined; (or, more verbosely, let x: T | undefined = undefined;). If your intention is that it will always be set before being read (whether or not this is provable from static analysis, e.g. due to setting the variable in a closure), let x: T is appropriate.

@kirkwaiblinger
Copy link
Member

kirkwaiblinger commented Jun 3, 2024

Note also that the code which is proposed to be allowed in this issue is dangerous code that hides a type error from TS. TS assumes that foo is not undefined. If it were possibly undefined, foo?.length would have type string | undefined, which is an error to compare to a number with the < operator. (playground)

let foo: string | undefined;

const setFoo = (): void => {
  if (foo?.length < 1) {
//    ~~~~~~~~~~~~~~~ TS ERROR 18048: 'foo.length' is possibly 'undefined'. 4:7 - 4:18     
    foo = 'foo';
  }
};

Furthermore, if we remove the optional chain, it's not a type error (playground)

let foo: string;

const setFoo = (): void => {
  if (foo.length < 1) {
    foo = 'foo';
  }
};

Which cannot be the case for removing an unnecessary optional chain on an accurately typed variable.

This is all to say, let foo: string; means "by the time you read foo it will be a string", and TS makes correctness assumptions about that with much broader implications than just the behavior in this rule. Therefore, regardless of how one feels about that decision on TS's part, I think it would be wrong to retcon it to mean anything else for this rule. Use let foo: string | undefined;.

@kirkwaiblinger
Copy link
Member

kirkwaiblinger commented Jun 3, 2024

Note from #9206 that var is also a relevant case:

if (Math.random()) {
  var x = 1;
}
if (x) {}

Fortunately, that's a TS use-before-define error 🙂, so I think we can avoid the mental strain of thinking about var, unless we can think of another case where it specifically matters.

@JacobLey

This comment was marked as off-topic.

@bradzacher

This comment was marked as off-topic.

@JacobLey

This comment was marked as off-topic.

@kirkwaiblinger
Copy link
Member

I think the rule's behavior is correct in this case. And I think that let x: T; should not be treated as T | undefined for the purposes of this rule, because it is not treated that way by TS.

Since I wrote this, it was pointed out to me by @bradzacher that this rule autofixes this case, which can cause unsafe behavior. That's a very good point!

I still believe strongly that the report is correct, but I agree that it's problematic to autofix this case. I believe the appropriate resolution would be to detect this case and downgrade the autofix to a suggestion.

I'd like to open this issue to PRs for that resolution if there are no objections?

@kirkwaiblinger
Copy link
Member

filed #9565 to propose flagging the code in the issue report for failing to initialize foo

@github-actions github-actions bot added the locked due to age Please open a new issue if you'd like to say more. See https://typescript-eslint.io/contributing. label Apr 30, 2025
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Apr 30, 2025
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
accepting prs Go ahead, send a pull request that resolves this issue bug Something isn't working locked due to age Please open a new issue if you'd like to say more. See https://typescript-eslint.io/contributing. package: eslint-plugin Issues related to @typescript-eslint/eslint-plugin
Projects
None yet
7 participants