Skip to content

feat(eslint-plugin): [no-unnecessary-type-conversion] add rule #10182

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

Merged
merged 46 commits into from
May 5, 2025

Conversation

skondrashov
Copy link
Contributor

@skondrashov skondrashov commented Oct 20, 2024

PR Checklist

Overview

Shows autofixable errors for:

// setup
let str = "string";
const num = 1;
const bool = true;
const big = BigInt(1);

// begin invalid cases

String("hello");
"hello".toString();
"" + "hello";
"hello" + "";

String(1 + "");
(1 + "").toString();
"" + (1 + "");
1 + "" + "";

String(str);
str.toString();
"" + str;
str + "";
str += "";

Number(+"1");
+(+"1");
~~+"1";

Number(num);
+num;
~~num;

Boolean(bool);
!!bool;

BigInt(big);

with the following error messaging:

   9:2  error  Passing a string to String() does not change the type or value of the string            @typescript-eslint/no-unnecessary-coercion
  10:9  error  Using .toString() on a string does not change the type or value of the string           @typescript-eslint/no-unnecessary-coercion
  11:2  error  Concatenating '' with a string does not change the type or value of the string          @typescript-eslint/no-unnecessary-coercion
  12:9  error  Concatenating a string with '' does not change the type or value of the string          @typescript-eslint/no-unnecessary-coercion
  14:2  error  Passing a string to String() does not change the type or value of the string            @typescript-eslint/no-unnecessary-coercion
  15:9  error  Using .toString() on a string does not change the type or value of the string           @typescript-eslint/no-unnecessary-coercion
  16:2  error  Concatenating '' with a string does not change the type or value of the string          @typescript-eslint/no-unnecessary-coercion
  17:8  error  Concatenating a string with '' does not change the type or value of the string          @typescript-eslint/no-unnecessary-coercion
  19:2  error  Passing a string to String() does not change the type or value of the string            @typescript-eslint/no-unnecessary-coercion
  20:5  error  Using .toString() on a string does not change the type or value of the string           @typescript-eslint/no-unnecessary-coercion
  21:2  error  Concatenating '' with a string does not change the type or value of the string          @typescript-eslint/no-unnecessary-coercion
  22:5  error  Concatenating a string with '' does not change the type or value of the string          @typescript-eslint/no-unnecessary-coercion
  23:5  error  Concatenating a string with '' does not change the type or value of the string          @typescript-eslint/no-unnecessary-coercion
  25:2  error  Passing a number to Number() does not change the type or value of the number            @typescript-eslint/no-unnecessary-coercion
  26:2  error  Using the unary + operator on a number does not change the type or value of the number  @typescript-eslint/no-unnecessary-coercion
  27:2  error  Using ~~ on a number does not change the type or value of the number                    @typescript-eslint/no-unnecessary-coercion
  29:2  error  Passing a number to Number() does not change the type or value of the number            @typescript-eslint/no-unnecessary-coercion
  30:2  error  Using the unary + operator on a number does not change the type or value of the number  @typescript-eslint/no-unnecessary-coercion
  31:2  error  Using ~~ on a number does not change the type or value of the number                    @typescript-eslint/no-unnecessary-coercion
  33:2  error  Passing a boolean to Boolean() does not change the type or value of the boolean         @typescript-eslint/no-unnecessary-coercion
  34:2  error  Using !! on a boolean does not change the type or value of the boolean                  @typescript-eslint/no-unnecessary-coercion
  36:2  error  Passing a bigint to BigInt() does not change the type or value of the bigint            @typescript-eslint/no-unnecessary-coercion

Does not (and should not) show errors for:

// setup
let str = Math.random() > 0.5 ? "string" : 1;
const num = Math.random() > 0.5 ? 1 : "1";
const bool = Math.random() > 0.5 ? true : 1;
const big = Math.random() > 0.5 ? BigInt(1) : 1;

// begin valid cases

String(str);
str.toString();
`${str}`;
"" + str;
str + "";
str += "";

new String("asdf");

Number(num);
+num;
~~num;
~1;

Boolean(bool);
!!bool;
!false;

BigInt(big);

Does not do anything with Symbol() because everything weird I tried to do with symbols was already a typescript error.

Does not do anything for some conversions to number which are already forbidden by typescript ("The left-hand side of an arithmetic operation must be of type 'any', 'number', 'bigint' or an enum type."), including:

"123" * 1;
"123" / 1;
"123" - 0;
"123" | 0;

Does not cover ${"123"}, because that overlaps with the no-unnecessary-template-expression rule. Need feedback on what to do about that.

I went with the word "coercion" because it was chosen by @Josh-Cena in the original issue but I think it's a poor choice. I think "conversion" or even "type conversion idiom" might be the most precise, because .toString() for example doesn't rely on coercion, and while !!value and !value both coerce the type of value to a boolean, it's the property of the idiom !! that it can be used for type conversion which puts it under the umbrella of this rule. So I think no-unnecessary-type-conversion is a better name for this rule, given the things that I'm currently having it check for.

Catches the following problems in this repository (separate commit):

typescript-eslint\packages\eslint-plugin\src\rules\adjacent-overload-signatures.ts
  79:21  error  Using !! on a boolean does not change the type or value of the boolean  @typescript-eslint/no-unnecessary-coercion
  97:21  error  Using !! on a boolean does not change the type or value of the boolean  @typescript-eslint/no-unnecessary-coercion

typescript-eslint\packages\eslint-plugin\src\rules\explicit-function-return-type.ts
  170:27  error  Using !! on a boolean does not change the type or value of the boolean  @typescript-eslint/no-unnecessary-coercion
  177:9   error  Using !! on a boolean does not change the type or value of the boolean  @typescript-eslint/no-unnecessary-coercion

typescript-eslint\packages\eslint-plugin\src\rules\member-ordering.ts
  492:14  error  Using !! on a boolean does not change the type or value of the boolean  @typescript-eslint/no-unnecessary-coercion

typescript-eslint\packages\eslint-plugin\src\rules\no-duplicate-enum-values.ts
  50:21  error  Passing a string to String() does not change the type or value of the string  @typescript-eslint/no-unnecessary-coercion
  52:21  error  Passing a number to Number() does not change the type or value of the number  @typescript-eslint/no-unnecessary-coercion

typescript-eslint\packages\eslint-plugin\src\rules\no-empty-interface.ts
  84:44  error  Using !! on a boolean does not change the type or value of the boolean  @typescript-eslint/no-unnecessary-coercion

typescript-eslint\packages\eslint-plugin\src\rules\no-redundant-type-constituents.ts
  176:10  error  Using !! on a boolean does not change the type or value of the boolean  @typescript-eslint/no-unnecessary-coercion

typescript-eslint\packages\eslint-plugin\src\rules\no-unnecessary-template-expression.ts
   22:24  error  Passing a string to String() does not change the type or value of the string  @typescript-eslint/no-unnecessary-coercion
  174:22  error  Passing a string to String() does not change the type or value of the string  @typescript-eslint/no-unnecessary-coercion

typescript-eslint\packages\eslint-plugin\src\rules\no-unsafe-assignment.ts
  202:17  error  Passing a string to String() does not change the type or value of the string  @typescript-eslint/no-unnecessary-coercion

typescript-eslint\packages\eslint-plugin\src\rules\prefer-return-this-type.ts
  64:14  error  Using !! on a boolean does not change the type or value of the boolean  @typescript-eslint/no-unnecessary-coercion

Might not have the best placement for the error scope, and might not have the best formatting for autofixes, could use feedback on those too.

@typescript-eslint
Copy link
Contributor

Thanks for the PR, @skondrashov!

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.

Copy link

netlify bot commented Oct 20, 2024

Deploy Preview for typescript-eslint ready!

Name Link
🔨 Latest commit 6822a69
🔍 Latest deploy log https://app.netlify.com/sites/typescript-eslint/deploys/68166b5ec9ca820008470ef2
😎 Deploy Preview https://deploy-preview-10182--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: 97 (🔴 down 1 from production)
Accessibility: 100 (no change from production)
Best Practices: 100 (no change from production)
SEO: 98 (no change from production)
PWA: 80 (no change from production)
View the detailed breakdown and full score reports

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

Copy link

nx-cloud bot commented Oct 20, 2024

View your CI Pipeline Execution ↗ for commit 6822a69.

Command Status Duration Result
nx test eslint-plugin ✅ Succeeded 6m 6s View ↗
nx run eslint-plugin:test -- --coverage ✅ Succeeded 6m 21s View ↗
nx test eslint-plugin --coverage=false ✅ Succeeded 5m 5s View ↗
nx run-many --target=lint --exclude eslint-plugin ✅ Succeeded 1m 20s View ↗
nx test visitor-keys ✅ Succeeded 1s View ↗
nx run types:build ✅ Succeeded 1s View ↗
nx typecheck ast-spec ✅ Succeeded 2s View ↗
nx run-many --target=build --exclude website --... ✅ Succeeded 7s View ↗
Additional runs (26) ✅ Succeeded ... View ↗

☁️ Nx Cloud last updated this comment at 2025-05-03 19:28:41 UTC

@skondrashov
Copy link
Contributor Author

skondrashov commented Oct 20, 2024

Sorry for force-push, wasn't sure how else to resolve the merge conflicts other than redoing the commit cause they didn't show up locally. Doesn't seem like it did anything to the merge conflict though, so I'm out of ideas.

I see I have problems from the perfectionist plugin in my files as well, but the plugin doesn't run locally when I use yarn lint, so I don't know what to do about that either.

Would be happy to address the 2 missing lines of coverage at some point if everything else gets figured out.

Copy link

codecov bot commented Oct 20, 2024

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 90.90%. Comparing base (bca8a91) to head (6822a69).
Report is 1 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main   #10182      +/-   ##
==========================================
+ Coverage   90.84%   90.90%   +0.06%     
==========================================
  Files         497      498       +1     
  Lines       50320    50655     +335     
  Branches     8311     8335      +24     
==========================================
+ Hits        45714    46049     +335     
  Misses       4591     4591              
  Partials       15       15              
Flag Coverage Δ
unittest 90.90% <100.00%> (+0.06%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
packages/eslint-plugin/src/configs/eslintrc/all.ts 100.00% <100.00%> (ø)
...lugin/src/configs/eslintrc/disable-type-checked.ts 100.00% <100.00%> (ø)
packages/eslint-plugin/src/configs/flat/all.ts 100.00% <100.00%> (ø)
...nt-plugin/src/configs/flat/disable-type-checked.ts 100.00% <100.00%> (ø)
...t-plugin/src/rules/adjacent-overload-signatures.ts 100.00% <100.00%> (ø)
...-plugin/src/rules/explicit-function-return-type.ts 100.00% <100.00%> (ø)
...ackages/eslint-plugin/src/rules/member-ordering.ts 99.20% <100.00%> (ø)
...slint-plugin/src/rules/no-duplicate-enum-values.ts 100.00% <100.00%> (ø)
...ages/eslint-plugin/src/rules/no-empty-interface.ts 100.00% <100.00%> (ø)
...plugin/src/rules/no-redundant-type-constituents.ts 97.64% <100.00%> (ø)
... and 5 more
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@kirkwaiblinger
Copy link
Member

kirkwaiblinger commented Oct 21, 2024

// setup
let str = Math.random() > 0.5 ? "string" : 1;
const num = Math.random() > 0.5 ? 1 : "1";
const bool = Math.random() > 0.5 ? true : 1;
const big = Math.random() > 0.5 ? BigInt(1) : 1;

(hint) - We often use declare to do this sort of setup for demo purposes, i.e.

// setup
declare let str: string | number;
declare const num: number | string;
// etc

It's a useful pattern for testing sometimes too, since you may run into surprising behavior when const t = true; has type true rather than type boolean. Nothing specifically requested, just letting you know about another tool for your kit in case it comes in handy during discussion on this PR 🙂

Does not do anything with Symbol() because everything weird I tried to do with symbols was already a typescript error.

Makes sense 👍

Does not do anything for some conversions to number which are already forbidden by typescript ("The left-hand side of an arithmetic operation must be of type 'any', 'number', 'bigint' or an enum type."), including:

"123" * 1;
"123" / 1;
"123" - 0;
"123" | 0;

Yep, also makes sense. And, even if TS did allow them, we don't need to stress about identifying every possible way for a value to be coerced, just the common patterns, and others can always be added in the future, too. Appreciate your close attention to detail on all these, though!

Does not cover ${"123"}, because that overlaps with the no-unnecessary-template-expression rule. Need feedback on what to do about that.

This is an astute observation. Is there anything this rule would flag that that rule doesn't flag? If not, I'd lean towards letting that separate rule handle this case. If you want you could even put a note in the docs that tells the user that template expressions intentionally aren't handled because they're already dealt with more thoroughly in the no-unnecessary-template-expression rule.

I went with the word "coercion" because it was chosen by @Josh-Cena in the original issue but I think it's a poor choice. I think "conversion" or even "type conversion idiom" might be the most precise, because .toString() for example doesn't rely on coercion, and while !!value and !value both coerce the type of value to a boolean, it's the property of the idiom !! that it can be used for type conversion which puts it under the umbrella of this rule. So I think no-unnecessary-type-conversion is a better name for this rule, given the things that I'm currently having it check for.

Hmm, I hear you. I personally think either one is fine. Maybe someone else will care more strongly 🤷

Might not have the best placement for the error scope, and might not have the best formatting for autofixes, could use feedback on those too.

I think for these reports, reporting the whole node is totally fine

declare const s: string;
s + '';
~~~~~~

!!(longExpressionThatsABoolean);
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

But, I really like what you've done with the more granular approach.

!!(longExpressionThatsABoolean);
~~

So I'd say go for it! But if it becomes an implementation hassle, no stress either if you end up falling back to the typical strategy of letting the node dictate the report.

@kirkwaiblinger
Copy link
Member

Sorry for force-push...

No worries! The real pain is just force pushing between reviews, not before a reviewer has looked at it at all 🙂.

..., wasn't sure how else to resolve the merge conflicts other than redoing the commit cause they didn't show up locally.

Oh, so I think the trouble that you're having is that you've used the main branch on your fork. The "usual" vanilla workflow would be

  1. fork typescript-eslint. The main branch of your fork points to the HEAD of main of the original typescript-eslint at the time the fork was made.
  2. Create a branch for your feature, work on there.
  3. Whenever you need to synchronize upstream changes, use the github UI or CLI to synchronize the changes on typescript-eslint's main with your fork's main. Then merge those into your branch.

But you're in a slightly awkward scenario since your branch is on main. What I personally do (which differs from the above) is to set a second remote on your local repo, i.e. git remote add upstream git@github.com:typescript-eslint/typescript-eslint.git. Then you can git fetch --all && git checkout upstream/main (or similar) to make a branch from there, to merge changes into your PR branch. There's... (too) many ways to address this, good ol git, haha.

LMK if this helps or if you'd like some more help. (I can also push things directly to your branch to bail you out if needed 🤣). And no stress if you end up doing a force push again this time to get yourself in a clean state here!

Copy link
Member

@kirkwaiblinger kirkwaiblinger left a comment

Choose a reason for hiding this comment

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

This is great work! And especially impressive considering it's your first PR to typescript-eslint. Well done!

Requesting a few changes, but nothing major.

🙂

@Josh-Cena
Copy link
Member

I went with the word "coercion" because it was chosen by @Josh-Cena in the original issue but I think it's a poor choice. I think "conversion" or even "type conversion idiom" might be the most precise, because .toString() for example doesn't rely on coercion, and while !!value and !value both coerce the type of value to a boolean, it's the property of the idiom !! that it can be used for type conversion which puts it under the umbrella of this rule. So I think no-unnecessary-type-conversion is a better name for this rule, given the things that I'm currently having it check for.

Agreed; let's go with no-unnecessary-type-conversion instead.

@kirkwaiblinger kirkwaiblinger added the awaiting response Issues waiting for a reply from the OP or another party label Nov 11, 2024
@JoshuaKGoldberg
Copy link
Member

👋 Just checking in @skondrashov - is this still something you have time and energy for?

@JoshuaKGoldberg JoshuaKGoldberg added the stale PRs or Issues that are at risk of being or have been closed due to inactivity for a prolonged period label Dec 14, 2024
@skondrashov
Copy link
Contributor Author

I'll have time to finish up the changes probably early in the new year!

@JoshuaKGoldberg
Copy link
Member

Awesome! I'll move this to draft then in the meantime, so we don't bug you. 🚀

@JoshuaKGoldberg JoshuaKGoldberg marked this pull request as draft December 21, 2024 14:50
@fregante
Copy link
Contributor

Excited about this rule! I just saw a lot of Boolean('x' in globalThis) in a repo

kirkwaiblinger
kirkwaiblinger previously approved these changes May 1, 2025
Copy link
Member

@kirkwaiblinger kirkwaiblinger left a comment

Choose a reason for hiding this comment

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

I think I'm happy with this! Thanks for your very detailed work on this. Outstanding first PR!

@kirkwaiblinger kirkwaiblinger added the 1 approval >=1 team member has approved this PR; we're now leaving it open for more reviews before we merge label May 1, 2025
@kirkwaiblinger kirkwaiblinger requested a review from a team May 1, 2025 15:36
@github-actions github-actions bot removed the 1 approval >=1 team member has approved this PR; we're now leaving it open for more reviews before we merge label May 1, 2025
kirkwaiblinger
kirkwaiblinger previously approved these changes May 2, 2025
@kirkwaiblinger kirkwaiblinger added the 1 approval >=1 team member has approved this PR; we're now leaving it open for more reviews before we merge label May 2, 2025
@kirkwaiblinger kirkwaiblinger requested a review from a team May 2, 2025 14:32
@github-actions github-actions bot removed the 1 approval >=1 team member has approved this PR; we're now leaving it open for more reviews before we merge label May 2, 2025
@kirkwaiblinger kirkwaiblinger added the 1 approval >=1 team member has approved this PR; we're now leaving it open for more reviews before we merge label May 2, 2025
@JoshuaKGoldberg
Copy link
Member

I am salivating at how many existing violations were removed in this PR. 🤤

JoshuaKGoldberg
JoshuaKGoldberg previously approved these changes May 2, 2025
Copy link
Member

@JoshuaKGoldberg JoshuaKGoldberg left a comment

Choose a reason for hiding this comment

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

Awesome, this is looking very good! A stellar first PR indeed 🏆. Thanks for pushing on this!

Just a few small things from me - nothing we can't just do before merge if you don't have the time.

@@ -4,7 +4,6 @@ import rule from '../../src/rules/await-thenable';
import { getFixturesRootDir } from '../RuleTester';

const rootDir = getFixturesRootDir();
const messageId = 'await';
Copy link
Member

Choose a reason for hiding this comment

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

Nit: it would be nice to have these split out into their own PR. I'm not going to block on this though.

@skondrashov skondrashov dismissed stale reviews from JoshuaKGoldberg and kirkwaiblinger via 6822a69 May 3, 2025 19:15
@github-actions github-actions bot removed the 1 approval >=1 team member has approved this PR; we're now leaving it open for more reviews before we merge label May 5, 2025
@kirkwaiblinger kirkwaiblinger added the 1 approval >=1 team member has approved this PR; we're now leaving it open for more reviews before we merge label May 5, 2025
Copy link
Member

@JoshuaKGoldberg JoshuaKGoldberg left a comment

Choose a reason for hiding this comment

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

Red wavy text with gradient yellow to reddish orange 3d effect: "WONDERFUL"

@JoshuaKGoldberg JoshuaKGoldberg merged commit ccbfcdc into typescript-eslint:main May 5, 2025
59 checks passed
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.

Rule proposal: no-unnecessary-coercion
6 participants