Skip to content

Bug: [no-unnecessary-condition] false positive with ??= operator and exactOptionalPropertyTypes compiler option #6635

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
4 tasks done
chharvey opened this issue Mar 14, 2023 · 9 comments · Fixed by #8249
Closed
4 tasks done
Labels
accepting prs Go ahead, send a pull request that resolves this issue bug Something isn't working package: eslint-plugin Issues related to @typescript-eslint/eslint-plugin

Comments

@chharvey
Copy link

chharvey commented Mar 14, 2023

Before You File a Bug Report Please Confirm You Have Done The Following...

  • I have tried restarting my IDE and the issue persists.
  • I have updated to the latest version of the packages.
  • I have searched for related issues and found none that matched my issue.
  • I have read the FAQ and my problem is not listed.

Playground Link

https://typescript-eslint.io/play/#ts=4.9.5&sourceType=module&code=MYewdgzgLgBCBGArAXDA3jADgJxJg-KgNpgCuAtvAKbYC6MAvjALzoMDcAUJwGaljAoAS3AwIVKAHkBVABQA3AIYAbUlVRlKNAJTFN1Ouk4wY2CaWxg4SAHQ48MfPlZElqqrXYwA9N5gBCGAABKABPTCoIYGwhTCgAWkjlITAobzAQeP4wKmBIiEVsUPjQMAATIWFwVAAiAFUwHLyIAqKYUoqqsBUAGhgqAA8IwSoymGUqHgSAC0VysSEyqjgeGAADJzW4COxFKBBsGH2YaiwQFqF4ZVCYMmVlOEP+JZ4U0ZsazgZOIA&eslintrc=N4KABGBEBOCuA2BTAzpAXGUEKQAIBcBPABxQGNoBLY-AWhXkoDt8B6Jge1tiacTJTIAhtEK0yHJgBNK+SpPRRE0aB2iRwYAL4gtQA&tsconfig=N4KABGBEDGD2C2AHAlgGwKYCcDyiAuysAdgM6QBcYoAkNZOgB4CG0euBxTqACprIljwBPACpCBZSnkwBXdABpwEKCWnJWAORmpUAYQAW6aAGtJYaXKUBfEFaA

Repro Code

const obj: { prop?: [number] } = {};

function setOnce(value: number): [number] {
    return obj.prop ??= [value]; // ! @typescript-eslint/no-unnecessary-condition: "Unnecessary conditional, expected left-hand side of `??` operator to be possibly null or undefined."
}

ESLint Config

module.exports = {
  parser: "@typescript-eslint/parser",
  rules: {
    "@typescript-eslint/no-unnecessary-condition": "error",
  },
};

tsconfig

{
  "compilerOptions": {
    "exactOptionalPropertyTypes": true,
    "strictNullChecks": true
  }
}

Expected Result

I expected the expression obj.prop ??= [value] to not report an error.

Since the property hasn’t been set yet, it is actually undefined at runtime.

Actual Result

The expression obj.prop ??= [value] is reported as an error.

typescript-eslint thinks obj.prop is non-nullish since exactOptionalPropertyTypes is on. We don’t want to turn it off because we still don’t want to allow statements like obj.prop = undefined;.

Additional Info

Related: #3553, #5344, #6632

@chharvey chharvey added bug Something isn't working package: eslint-plugin Issues related to @typescript-eslint/eslint-plugin triage Waiting for team members to take a look labels Mar 14, 2023
@chharvey
Copy link
Author

#3553 (comment):

I think I found a bug when tsconfig’s exactOptionalPropertyTypes is on. With this setting, optional properties are treated as nullish when accessed but as non-nullish when assigned.

// tsconfig:compilerOptions.exactOptionalPropertyTypes = true

const obj: { prop?: [number] } = {};

obj.prop;
//  ^? (property) prop?: [number] | undefined

obj.prop = [42];
//  ^? (property) prop?: [number]

I have a function that sets the property once and returns it on the first call, but subsequent calls only return it without setting it:

function setOnce(value: number): [number] {
    return obj.prop ??= [value]; // ! @typescript-eslint/no-unnecessary-condition: "Unnecessary conditional, expected left-hand side of `??` operator to be possibly null or undefined."
}

setOnce(1); //=> [1]
setOnce(2); //=> [1]
obj.prop.push(3);
setOnce(1);  //=> [1, 3]

The problem here is that typescript-eslint thinks obj.prop is non-nullish and reports an error, even though it is nullish before being assigned.

I’m not sure what the right solution is. We want to keep --exactOptionalPropertyTypes on to avoid statements like obj.prop = undefined;, but we still want to allow the ??= assignment operator.

@bradzacher bradzacher added accepting prs Go ahead, send a pull request that resolves this issue and removed triage Waiting for team members to take a look labels Mar 15, 2023
karlhorky added a commit to upleveled/ical-move-events that referenced this issue Mar 15, 2023
* Shorten long recurring events to end date

* Add missing imports, switch to named imports

* Update snapshots

* Fail on ESLint warnings

* Increase max diff lines for Vitest

* Downgrade packages for false positive

Ref: typescript-eslint/typescript-eslint#6632
Ref: typescript-eslint/typescript-eslint#6635

* Fix import

* Revert "Fix import"

This reverts commit d194a25.
@bradzacher
Copy link
Member

Filed a bug with TS - microsoft/TypeScript#53305
IMO this should be fixed in TS itself cos even TS behaves differently with assignment operators vs their expanded form. But we'll see what they say.

If not, then we either must implement our own logic to determine if the LHS might be an exact optional type or not, or we might just disable checks on ??=. First port of call is the types, then we can make a decision about next steps.

@chharvey
Copy link
Author

@bradzacher — I believe TS is working as intended since after assignment it will be number and not undefined. We’ll see what they say. Regardless, I would add this function to your playground, for completeness:

function ee() {
xx.prop = 1;
//  ^?
// typeof xx.prop === number ❌
}

@bradzacher
Copy link
Member

bradzacher commented Mar 17, 2023

@chharvey if it was working as intended then += and ??= would work the same way.

(number | undefined) + number === number always (because NaN is a number).
So by your reasoning:

xx.prop += 1
//  ^?
// typeof xx.prop === number, because the result of the assignment operator will always be number

But that's not what we see from TS - we see that TS correctly shows that typeof xx.prop === number | undefined.
Which is why it's inconsistent!

You need to separate the read from the write!
xx.prop has not been updated until after the assignment node is complete and traversed.
i.e.

xx.prop += 1;
            ^ semi = end of expression = assignment complete
              before this point xx.prop has not been updated
              so any intellisense hovers should be the type *before* the write

TS does appear to do this - but from the looks of it TS also does some funny stuff with the type of a variable that has a union type at write time - it appears to invalidate the refinements on the variable at specific write times.

@rgant
Copy link

rgant commented Apr 10, 2023

This is filling up my code with a bunch of // eslint-disables. Anything that can be done about this?

@laverdet
Copy link

This issue hits our team really bad. We're currently working around this with a local patch via pnpm:

diff --git a/patches/@typescript-eslint__eslint-plugin@5.58.0.patch b/patches/@typescript-eslint__eslint-plugin@5.58.0.patch
new file mode 100644
index 000000000..c38427dcd
--- /dev/null
+++ b/patches/@typescript-eslint__eslint-plugin@5.58.0.patch
@@ -0,0 +1,12 @@
+diff --git a/dist/rules/no-unnecessary-condition.js b/dist/rules/no-unnecessary-condition.js
+index 6d36b811889c22e0b6dc61beb40a82671fd47ad4..d0f06ace52ecbffe4b571ebc9d05450dca15a1fb 100644
+--- a/dist/rules/no-unnecessary-condition.js
++++ b/dist/rules/no-unnecessary-condition.js
+@@ -479,7 +479,6 @@ exports.default = (0, util_1.createRule)({
+             }
+         }
+         return {
+-            AssignmentExpression: checkAssignmentExpression,
+             BinaryExpression: checkIfBinaryExpressionIsNecessaryConditional,
+             CallExpression: checkCallExpression,
+             ConditionalExpression: (node) => checkNode(node.test),

@karlhorky
Copy link

It seems like the TypeScript PR microsoft/TypeScript#54777 was merged, which closed the issue in the TypeScript repo:

Will this mean that this issue will "fix itself" once the TS PR changes are released in a version of TypeScript (eg. maybe 5.2)?

@stianjensen
Copy link

Will this mean that this issue will "fix itself" once the TS PR changes are released in a version of TypeScript (eg. maybe 5.2)?

Running with 5.3-beta now seems to still trigger the warning for me, so I'm assuming some fix is also needed here?

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 package: eslint-plugin Issues related to @typescript-eslint/eslint-plugin
Projects
None yet
7 participants