Skip to content

fix(eslint-plugin): [prefer-optional-chain] use suggestion instead of autofix for trailing binary operator#12328

Merged
JoshuaKGoldberg merged 1 commit into
typescript-eslint:mainfrom
mdm317:fix/11917-prefer-optional-chain-binaryoperator
Jun 28, 2026
Merged

fix(eslint-plugin): [prefer-optional-chain] use suggestion instead of autofix for trailing binary operator#12328
JoshuaKGoldberg merged 1 commit into
typescript-eslint:mainfrom
mdm317:fix/11917-prefer-optional-chain-binaryoperator

Conversation

@mdm317

@mdm317 mdm317 commented May 12, 2026

Copy link
Copy Markdown
Contributor

PR Checklist

Overview

  • Downgrades the trailing-chain autofix added in pr-11533 to a suggestion.
  • Pre-existing trailing nullish-equality chains before [pr-11533] (e.g. foo == null || foo.bar == undefined) are still autofixed.
    • Should we also change the above case in this PR?
      • If the last chain uses any binary operator, should we also switch it to a suggestion?

Additional Notes

It seems the unsafe rewrites only occur when the compared value is non-primitive.
When the compared value is primitive, the rewrite appears to be safe.

Possible options:

  • keep it as an autofix, or
  • gate it behind an opt-in option

@typescript-eslint

Copy link
Copy Markdown
Contributor

Thanks for the PR, @mdm317!

typescript-eslint is a 100% community driven project, and we are incredibly grateful that you are contributing to that community.

The core maintainers work on this in their personal time, so please understand that it may not be possible for them to review your work immediately.

Thanks again!


🙏 Please, if you or your company is finding typescript-eslint valuable, help us sustain the project by sponsoring it transparently on https://opencollective.com/typescript-eslint.

@netlify

netlify Bot commented May 12, 2026

Copy link
Copy Markdown

Deploy Preview for typescript-eslint ready!

Name Link
🔨 Latest commit 34b96b4
🔍 Latest deploy log https://app.netlify.com/projects/typescript-eslint/deploys/6a0300f90f76270008b7cce8
😎 Deploy Preview https://deploy-preview-12328--typescript-eslint.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
Lighthouse
Lighthouse
1 paths audited
Performance: 98 (🔴 down 1 from production)
Accessibility: 97 (no change from production)
Best Practices: 100 (no change from production)
SEO: 90 (no change from production)
PWA: 80 (no change from production)
View the detailed breakdown and full score reports
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@nx-cloud

nx-cloud Bot commented May 12, 2026

Copy link
Copy Markdown

View your CI Pipeline Execution ↗ for commit 34b96b4

Command Status Duration Result
nx test rule-tester -- ✅ Succeeded 3s View ↗
nx test tsconfig-utils -- ✅ Succeeded <1s View ↗
nx test utils -- ✅ Succeeded 10s View ↗
nx run types:build ✅ Succeeded 2s View ↗
nx test eslint-plugin-internal -- ✅ Succeeded 3s View ↗

☁️ Nx Cloud last updated this comment at 2026-05-18 17:14:24 UTC

@mdm317 mdm317 marked this pull request as ready for review May 12, 2026 10:41
@kirkwaiblinger

Copy link
Copy Markdown
Member
  • Pre-existing trailing nullish-equality chains before [pr-11533] (e.g. foo == null || foo.bar == undefined) are still autofixed.

    • Should we also change the above case in this PR?

It would help to have a complete example, but the following is sound and doesn't need to be changed:

Playground

declare const foo: { bar: string | undefined } | null
foo == null || foo.bar == undefined
* If the last chain uses any binary operator, should we also switch it to a suggestion?

Can you provide an example of what you mean?

Additional Notes

It seems the unsafe rewrites only occur when the compared value is non-primitive. When the compared value is primitive, the rewrite appears to be safe.

Possible options:

  • keep it as an autofix, or
  • gate it behind an opt-in option

Can you provide an example of what you mean?

@kirkwaiblinger kirkwaiblinger left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The actual code changes here look good to me! And all the test cases I looked at looked like the correct change. Didn't check closely yet whether there any missed cases...

Would like some clarity on the questions in the PR description, though, before doing a thorough pass for any missed cases.

Thanks!

@kirkwaiblinger kirkwaiblinger added the awaiting response Issues waiting for a reply from the OP or another party label May 18, 2026
@mdm317

mdm317 commented May 28, 2026

Copy link
Copy Markdown
Contributor Author

Sorry — my previous reply had mistakes in both the explanation and the examples.
Here's a corrected version.

Re: Q1 & Q2

My original example was wrong.

There are still a few cases where the last operand is neither a nullish check nor a truthy/falsy check.
Should we keep those as autofixes?

declare const foo: { bar: number };
!foo || foo.bar === null;

Personally, I'd recommend downgrading this not reporting it at all.
The code above can still cause issues such as #11700
See also @kirkwaiblinger's comment in #11700.

Re: Q3

The RHS constraint is literal, not primitive — I misspoke earlier.
Please disregard my comments about the opt-in option.

I'd suggest restricting the autofix to cases where all three conditions below hold.

  1. The chain should be used in a boolean context.

    if (foo && foo.bar === 5) { /* safe */ }
    const x = foo && foo.bar === 5; // type of x changes
  2. The RHS should be a literal.

    A non-literal RHS breaks the algorithm implemented in #11533.
    Two such cases are currently open:

    • May be undefined at runtime but not in the type (e.g. array / Record)
      (#11917):
      foo && foo.bar === arr[4]
    • RHS references the chain itself
      (#12204, #12200):
      a && a.b === a
  3. No prior operand should be a truthy/falsy check

    declare const s: string;
    
    // before — when s is ""
    !s || s.lastIndexOf('.') !== -1; // → true
    // after autofix — when s is ""
    s?.lastIndexOf('.') !== -1;      // → false

If anything is unclear or seems off, I'd appreciate a follow-up — happy to clarify.

@mdm317 mdm317 requested a review from kirkwaiblinger May 28, 2026 18:33
@github-actions github-actions Bot removed the awaiting response Issues waiting for a reply from the OP or another party label May 28, 2026
@JoshuaKGoldberg

JoshuaKGoldberg commented Jun 15, 2026

Copy link
Copy Markdown
Member

@mdm317 can you confirm you stand behind through your response? The one you most recently posted looks to be pretty directly AI copy & paste. https://typescript-eslint.io/contributing/ai-policy

@JoshuaKGoldberg JoshuaKGoldberg added the awaiting response Issues waiting for a reply from the OP or another party label Jun 15, 2026
@mdm317

mdm317 commented Jun 20, 2026

Copy link
Copy Markdown
Contributor Author

@JoshuaKGoldberg
Yes, I stand behind my response.

I used AI to help translate my thoughts into English, not to generate the response itself, which may have made the wording sound AI-generated.

I've updated the comment to better explain my reasoning.
If anything seems unclear, please let me know.

@kirkwaiblinger kirkwaiblinger left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry — my previous reply had mistakes in both the explanation and the examples. Here's a corrected version.

Re: Q1 & Q2

My original example was wrong.

There are still a few cases where the last operand is neither a nullish check nor a truthy/falsy check. Should we keep those as autofixes?

declare const foo: { bar: number };
!foo || foo.bar === null;

Personally, I'd recommend downgrading this not reporting it at all. The code above can still cause issues such as #11700 See also @kirkwaiblinger's comment in #11700.

As far as this PR, I don't think we change anything here. Note that it's already a suggestion currently, not an autofix.

Re: Q3

The RHS constraint is literal, not primitive — I misspoke earlier. Please disregard my comments about the opt-in option.

I'd suggest restricting the autofix to cases where all three conditions below hold.

  1. The chain should be used in a boolean context.

    if (foo && foo.bar === 5) { /* safe */ }
    const x = foo && foo.bar === 5; // type of x changes
  2. The RHS should be a literal.
    A non-literal RHS breaks the algorithm implemented in #11533.
    Two such cases are currently open:

    • May be undefined at runtime but not in the type (e.g. array / Record)
      (#11917):
      foo && foo.bar === arr[4]
    • RHS references the chain itself
      (#12204, #12200):
      a && a.b === a
  3. No prior operand should be a truthy/falsy check

    declare const s: string;
    
    // before — when s is ""
    !s || s.lastIndexOf('.') !== -1; // → true
    // after autofix — when s is ""
    s?.lastIndexOf('.') !== -1;      // → false

If anything is unclear or seems off, I'd appreciate a follow-up — happy to clarify.

This PR currently downgrades these cases, which were previously auto-fixed, to a suggestion, which is the expected resolution based on #11917 (comment).

If I understand correctly, the question is whether the autofix should be preserved in special cases that actually can be proven to be safe (satisfying the three conditions above), rather than downgraded to suggestions along with the rest of the cases? I'd say, no, let's not bother, because a) it's of marginal value b) it adds complexity, and c) it will delay merging this PR, which I'm anxious to merge 😄.


Regarding #12328 (comment), translating is a completely valid and understandable use of AI, thank you for letting us know 👍 For whatever reason, the wording in this PR turned out to be rather difficult to understand... You've submitted quite a few high quality PRs previously (which we really appreciate! ❤️) and I think we've always had an easy time understanding the conversations in them, so it was just a bit surprising to see a difference here (and we're bit apprehensive in general these days due to the increasing volume of bad, AI-authored PRs).

In any case, this looks good to me. Thanks for sending! 🙏

@kirkwaiblinger kirkwaiblinger added 1 approval >=1 team member has approved this PR; we're now leaving it open for more reviews before we merge and removed awaiting response Issues waiting for a reply from the OP or another party labels Jun 21, 2026
@mdm317

mdm317 commented Jun 21, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the detailed explanation and review!

And thanks for the kind words as well 😊
I'll try to make future PR descriptions a bit clearer.

@JoshuaKGoldberg JoshuaKGoldberg merged commit ebce2d4 into typescript-eslint:main Jun 28, 2026
115 of 119 checks passed
renovate Bot added a commit to andrei-picus-tink/auto-renovate that referenced this pull request Jul 1, 2026
| datasource | package                          | from   | to     |
| ---------- | -------------------------------- | ------ | ------ |
| npm        | @typescript-eslint/eslint-plugin | 8.61.0 | 8.62.1 |
| npm        | @typescript-eslint/parser        | 8.61.0 | 8.62.1 |


## [v8.62.1](https://github.com/typescript-eslint/typescript-eslint/blob/HEAD/packages/eslint-plugin/CHANGELOG.md#8621-2026-06-29)

##### 🩹 Fixes

- **eslint-plugin:** \[no-unnecessary-type-assertion] parenthesize object literal at left edge of expression statement ([#12443](typescript-eslint/typescript-eslint#12443), [#12418](typescript-eslint/typescript-eslint#12418))
- **eslint-plugin:** \[no-unnecessary-boolean-literal-compare] preserve boolean result in fixer for nullable true comparisons ([#12365](typescript-eslint/typescript-eslint#12365))
- **eslint-plugin:** \[prefer-optional-chain] use suggestion instead of autofix for trailing binary operator ([#12328](typescript-eslint/typescript-eslint#12328))

##### ❤️ Thank You

- Kirk Waiblinger [@kirkwaiblinger](https://github.com/kirkwaiblinger)
- mdm317
- Patrick Aleite
- 송재욱

See [GitHub Releases](https://github.com/typescript-eslint/typescript-eslint/releases/tag/v8.62.1) for more information.

You can read about our [versioning strategy](https://typescript-eslint.io/users/versioning) and [releases](https://typescript-eslint.io/users/releases) on our website.


## [v8.62.0](https://github.com/typescript-eslint/typescript-eslint/blob/HEAD/packages/eslint-plugin/CHANGELOG.md#8620-2026-06-22)

##### 🚀 Features

- remove redundant package.json "files" ([#12444](typescript-eslint/typescript-eslint#12444))

##### ❤️ Thank You

- Kirk Waiblinger [@kirkwaiblinger](https://github.com/kirkwaiblinger)

See [GitHub Releases](https://github.com/typescript-eslint/typescript-eslint/releases/tag/v8.62.0) for more information.

You can read about our [versioning strategy](https://typescript-eslint.io/users/versioning) and [releases](https://typescript-eslint.io/users/releases) on our website.


## [v8.61.1](https://github.com/typescript-eslint/typescript-eslint/blob/HEAD/packages/eslint-plugin/CHANGELOG.md#8611-2026-06-15)

##### 🩹 Fixes

- **eslint-plugin:** \[no-unnecessary-template-expression] respect ECMAScript line terminators ([#12388](typescript-eslint/typescript-eslint#12388))
- **eslint-plugin:** \[no-unnecessary-boolean-literal-compare] fix precedence bug in autofix ([#12413](typescript-eslint/typescript-eslint#12413))
- **eslint-plugin:** \[no-unnecessary-type-assertion] wrap object literal in parens when removing TSTypeAssertion in arrow body ([#12394](typescript-eslint/typescript-eslint#12394), [#12393](typescript-eslint/typescript-eslint#12393))
- **eslint-plugin:** \[no-unnecessary-type-assertion] avoid false positive for template literal expressions ([#12281](typescript-eslint/typescript-eslint#12281))
- **eslint-plugin:** \[consistent-indexed-object-style] do not remove comments when fixing ([#12396](typescript-eslint/typescript-eslint#12396), [#10577](typescript-eslint/typescript-eslint#10577))

##### ❤️ Thank You

- Anas [@anasm266](https://github.com/anasm266)
- Deftera [@Deftera186](https://github.com/Deftera186)
- Kirk Waiblinger [@kirkwaiblinger](https://github.com/kirkwaiblinger)
- lumir
- Sarath Francis [@sarathfrancis90](https://github.com/sarathfrancis90)

See [GitHub Releases](https://github.com/typescript-eslint/typescript-eslint/releases/tag/v8.61.1) for more information.

You can read about our [versioning strategy](https://typescript-eslint.io/users/versioning) and [releases](https://typescript-eslint.io/users/releases) on our website.
renovate Bot added a commit to andrei-picus-tink/auto-renovate that referenced this pull request Jul 4, 2026
| datasource | package                          | from   | to     |
| ---------- | -------------------------------- | ------ | ------ |
| npm        | @typescript-eslint/eslint-plugin | 8.61.0 | 8.62.1 |
| npm        | @typescript-eslint/parser        | 8.61.0 | 8.62.1 |


## [v8.62.1](https://github.com/typescript-eslint/typescript-eslint/blob/HEAD/packages/eslint-plugin/CHANGELOG.md#8621-2026-06-29)

##### 🩹 Fixes

- **eslint-plugin:** \[no-unnecessary-type-assertion] parenthesize object literal at left edge of expression statement ([#12443](typescript-eslint/typescript-eslint#12443), [#12418](typescript-eslint/typescript-eslint#12418))
- **eslint-plugin:** \[no-unnecessary-boolean-literal-compare] preserve boolean result in fixer for nullable true comparisons ([#12365](typescript-eslint/typescript-eslint#12365))
- **eslint-plugin:** \[prefer-optional-chain] use suggestion instead of autofix for trailing binary operator ([#12328](typescript-eslint/typescript-eslint#12328))

##### ❤️ Thank You

- Kirk Waiblinger [@kirkwaiblinger](https://github.com/kirkwaiblinger)
- mdm317
- Patrick Aleite
- 송재욱

See [GitHub Releases](https://github.com/typescript-eslint/typescript-eslint/releases/tag/v8.62.1) for more information.

You can read about our [versioning strategy](https://typescript-eslint.io/users/versioning) and [releases](https://typescript-eslint.io/users/releases) on our website.


## [v8.62.0](https://github.com/typescript-eslint/typescript-eslint/blob/HEAD/packages/eslint-plugin/CHANGELOG.md#8620-2026-06-22)

##### 🚀 Features

- remove redundant package.json "files" ([#12444](typescript-eslint/typescript-eslint#12444))

##### ❤️ Thank You

- Kirk Waiblinger [@kirkwaiblinger](https://github.com/kirkwaiblinger)

See [GitHub Releases](https://github.com/typescript-eslint/typescript-eslint/releases/tag/v8.62.0) for more information.

You can read about our [versioning strategy](https://typescript-eslint.io/users/versioning) and [releases](https://typescript-eslint.io/users/releases) on our website.


## [v8.61.1](https://github.com/typescript-eslint/typescript-eslint/blob/HEAD/packages/eslint-plugin/CHANGELOG.md#8611-2026-06-15)

##### 🩹 Fixes

- **eslint-plugin:** \[no-unnecessary-template-expression] respect ECMAScript line terminators ([#12388](typescript-eslint/typescript-eslint#12388))
- **eslint-plugin:** \[no-unnecessary-boolean-literal-compare] fix precedence bug in autofix ([#12413](typescript-eslint/typescript-eslint#12413))
- **eslint-plugin:** \[no-unnecessary-type-assertion] wrap object literal in parens when removing TSTypeAssertion in arrow body ([#12394](typescript-eslint/typescript-eslint#12394), [#12393](typescript-eslint/typescript-eslint#12393))
- **eslint-plugin:** \[no-unnecessary-type-assertion] avoid false positive for template literal expressions ([#12281](typescript-eslint/typescript-eslint#12281))
- **eslint-plugin:** \[consistent-indexed-object-style] do not remove comments when fixing ([#12396](typescript-eslint/typescript-eslint#12396), [#10577](typescript-eslint/typescript-eslint#10577))

##### ❤️ Thank You

- Anas [@anasm266](https://github.com/anasm266)
- Deftera [@Deftera186](https://github.com/Deftera186)
- Kirk Waiblinger [@kirkwaiblinger](https://github.com/kirkwaiblinger)
- lumir
- Sarath Francis [@sarathfrancis90](https://github.com/sarathfrancis90)

See [GitHub Releases](https://github.com/typescript-eslint/typescript-eslint/releases/tag/v8.61.1) for more information.

You can read about our [versioning strategy](https://typescript-eslint.io/users/versioning) and [releases](https://typescript-eslint.io/users/releases) on our website.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

1 approval >=1 team member has approved this PR; we're now leaving it open for more reviews before we merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: [prefer-optional-chain] invalid fix with array access by index

3 participants