Repository: typescript-eslint/typescript-eslint
Pull request #10182: feat(eslint-plugin): [no-unnecessary-type-conversion] add rule
Author: login: skondrashov
Labels: name: 1 approval
description: >=1 team member has approved this PR; we're now leaving it open for more reviews before we merge
Created: 2024-10-20T17:03:21Z | Merged: 2025-05-05T15:09:57Z

## PR Checklist

- [x] Addresses an existing open issue: fixes #8515
- [x] That issue was marked as [accepting prs](https://github.com/typescript-eslint/typescript-eslint/issues?q=is%3Aopen+is%3Aissue+label%3A%22accepting+prs%22)
- [x] Steps in [Contributing](https://typescript-eslint.io/contributing) were taken

## Overview

Shows autofixable errors for:

```typescript
// 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:

```typescript
// 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:

```typescript
"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.

Linked issue #8515: Rule proposal: no-unnecessary-coercion
### Before You File a Proposal Please Confirm You Have Done The Following...

- [X] I have [searched for related issues](https://github.com/typescript-eslint/typescript-eslint/issues?q=is%3Aissue+label%3A%22enhancement%3A+new+plugin+rule%22) and found none that match my proposal.
- [X] I have searched the [current rule list](https://typescript-eslint.io/rules/#rules) and found no rules that match my proposal.
- [X] I have [read the FAQ](https://typescript-eslint.io/linting/troubleshooting) and my problem is not listed.

### My proposal is suitable for this project

- [X] My proposal specifically checks TypeScript syntax, or it proposes a check that requires type information to be accurate.
- [X] My proposal is not a "formatting rule"; meaning it does not just enforce how code is formatted (whitespace, brace placement, etc).
- [X] I believe my proposal would be useful to the broader TypeScript community (meaning it is not a niche proposal).

### Description

I'd like `String(something)`, `something.toString()`, and perhaps `something as string` to show an error if something is a `string` already, but to be allowed if it's a `string | number` or even a `string | number | undefined`. This is similar to the proposal at https://github.com/typescript-eslint/typescript-eslint/issues/6385 but it appears there're some issues with doing this with numbers that I don't think applies to strings.

In my case, I would like to do this because I am working with a library that exports a `string | number | undefined`, but I believe their type is incorrect, and I would like to essentially be notified that I can remove my casts if the type changes and becomes a `string` instead, and I believe similar use cases would not be exceeding rare, where one might be able to catch useless casts after updating one type somewhere, whether in a library or in their own type definitions. My feeling is that if this were turned on it would trigger errors in more than a couple of codebases. In reference to the similar issue for numbers, I do support the implementation of that as well for similar reasons.

### Fail Cases

```TypeScript
let replace = 'me';
replace = String(replace)
```


### Pass Cases

```TypeScript
let replace: string | number = 'me';
replace = String(replace)
```


### Additional Info

This is also similar to eslint's https://eslint.org/docs/latest/rules/no-extra-boolean-cast, which I'm actually not sure how they achieve without typescript, but that rule makes a lot of sense to me as well.
Linked issue comments: user: login: bradzacher
created_at: 2024-02-20T20:19:37Z
body: > This is also similar to eslint's [eslint.org/docs/latest/rules/no-extra-boolean-cast](https://eslint.org/docs/latest/rules/no-extra-boolean-cast), which I'm actually not sure how they achieve without typescript, but that rule makes a lot of sense to me as well.

That rule exists just to do "if the `Boolean()` call is within a conditional test - error". It doesn't need types because it doesn't check the value passed at all.

A similar rule for `String` would be "if the `String()` call is within a sum with a string - error" (eg `String(x) + ''` would always error).
author_association: MEMBER
user: login: bradzacher
created_at: 2024-02-20T21:34:37Z
body: > but it appears there're some issues with doing this with numbers that I don't think applies to strings

I'm not sure what "issues" you're referring to.
But IMO the same arguments apply here. EG similar to what is mentioned in https://github.com/typescript-eslint/typescript-eslint/issues/6385#issuecomment-1408146956 -- [step 1 of the string coercion algorithm](https://tc39.es/ecma262/multipage/abstract-operations.html#sec-tostring) is "if string return" - so there's near zero perf hit from an unnecessary `String()` call - it's just a stylistic thing.

Otherwise our big argument was that it's not _super_ common to use `String()`/`Number()`/`Boolean()` in modern TS, let alone having provably useless calls - so a rule warning against unnecessary calls isn't super high-value. Just not sure if people would find value in such a rule.
author_association: MEMBER
user: login: skondrashov
created_at: 2024-02-21T04:14:07Z
body: Thank you for the clarification on the Boolean rule! I do think then that something like this could cover `!!`, `Boolean()`, and `as boolean` casts of `boolean`s as well.

The issue I was referring to was that for numbers, Number() has problems if it takes things like strings of spaces, so parseInt() is a better idea, and since parseInt() does not accept a string the rule is unnecessary. On the other hand for strings, String() and .toString() both work with `string | number` and I don't think they have any weird issues for numbers, at least for integers. For booleans as well, the dynamic casts react to many types intuitively.

I don't have a sense for how common it is to do casts to strings or otherwise. I feel like the rule is nice for `as string`/`as number`/`as boolean` as well, and I feel like it must be quite common for at least one kind of cast, whether it's JS or TS, to happen at the interface with form elements. It seems like it's easy for this problem to happen when a type changes from `string | number` to `string`, or from any broad type to a specific type and you don't realize you were doing a cast somewhere, and it's unambiguous that it's wrong to cast a thing to its own type too I think. As a user I kind of expect a linter to lean towards detecting things that are very obviously wrong rather than things that are very common (though "very harmful" makes sense as a metric too, and I agree that these mistakes aren't particularly harmful). Makes sense that you'd want community support, but I'd be happy to try the implementation myself too if it's about managing what you spend time on.
author_association: CONTRIBUTOR
user: login: JoshuaKGoldberg
created_at: 2024-02-21T13:11:16Z
body: Aside: this is a good example of why I'm real strict about separating "type assertion" and "cast" as terms. This issue is correct and talking about _casting_. But a lot of TS devs refer to type assertions as casting as well. They're not the same! 

(ignore me, just ranting some contextual information)
author_association: MEMBER
user: login: skondrashov
created_at: 2024-02-21T16:15:25Z
body: In my head I kind of use "dynamic casting" for `String()` and "static casting" for `as string`, but yeah very important to distinguish them! I feel like this rule could work for both which furthers the confusion but yeah.
author_association: CONTRIBUTOR
user: login: skondrashov
created_at: 2024-03-01T20:05:31Z
body: Another case occured to me that is in-line with my feelings on this, and apparently is already covered by another rule:
https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/docs/rules/no-unnecessary-condition.md

From my perspective, checking to see if something is already guaranteed to be a string when it is cast to or asserted to be a `string` is similar to checking if something is already guaranteed not to be `undefined` or `null` when used, for instance, with the optional chaining (`?.`) operator. I understand that branching conditionally on the type of a value is a thing that is yet again different from both casting a value and asserting a value's type, but in practice they are all ways for a user to get typescript to believe that a variable is a certain type, and I think linting rules are helpful which let a user know that typescript already believes them and they don't need the branch, cast, or type assertion.

Maybe neither here nor there, but just a thought.
author_association: CONTRIBUTOR
user: login: skondrashov
created_at: 2024-03-07T18:24:38Z
body: I missed somehow that no-unnecessary-type-assertion is already a rule, so this would indeed apply only to actual runtime casting. I was able to set up a development environment and started trying to make an implementation, I know there's a good chance that this will be closed without community support but I'd like the rule anyway so I'm going to give it a shot.
author_association: CONTRIBUTOR
user: login: kirkwaiblinger
created_at: 2024-03-13T02:22:58Z
body: I'm +1 on this. The typescript-eslint codebase itself has unnecessary `!!` that could be linted out, see 

https://github.com/typescript-eslint/typescript-eslint/blob/fc08f969e255b55ea35091b297b358bc434a76eb/packages/eslint-plugin/src/rules/prefer-regexp-exec.ts#L75-L90
author_association: MEMBER
user: login: Josh-Cena
created_at: 2024-06-02T07:46:46Z
body: ~~Duplicate of https://github.com/typescript-eslint/typescript-eslint/issues/3204~~

Closed the other one
author_association: MEMBER
user: login: Josh-Cena
created_at: 2024-06-02T07:48:24Z
body: For the record I'm strongly in favor of reporting all unnecessary type coercions. Not just `String()` but `+`, `!!`, `BigInt` etc. too
author_association: MEMBER
user: login: skondrashov
created_at: 2024-06-15T05:08:38Z
body: I implemented this, with tests, fix functions, and docs which pass yarn test, over at https://github.com/skondrashov/typescript-eslint

It gives autofixable errors for:
```typescript
// 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);
```

and allows:

```typescript
// 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);
```

It does not do anything with `Symbol()` because everything weird I tried to do with symbols was already a typescript error. Similarly, a sizable set of conversions to `number` via coercion are also forbidden by typescript ("`The left-hand side of an arithmetic operation must be of type 'any', 'number', 'bigint' or an enum type.`"), including:

```typescript
"123" * 1;
"123" / 1;
"123" - 0;
"123" | 0;
```

It also doesn't cover `` `${"123"}` ``, because that overlaps with the `no-useless-template-literals` rule and I don't know how to resolve that.

One thing is I don't think the word "coercion" is entirely accurate for this rule. 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 that puts it under the umbrella of this rule. So I think `no-unnecessary-type-conversion` is a better name for this, given the things that I'm currently having it check for.

I haven't opened a PR since this isn't currently accepting PRs, but I'm close to being ready to.

Catches these errors within @typescript-eslint, which appear to be valid except for `no-duplicate-enum-values` where it's not clear to me what the author intended:

```
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
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
eslint-plugin\src\rules\member-ordering.ts
  486:14  error  Using !! on a boolean does not change the type or value of the boolean  @typescript-eslint/no-unnecessary-coercion
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
eslint-plugin\src\rules\no-empty-interface.ts
  83:44  error  Using !! on a boolean does not change the type or value of the boolean  @typescript-eslint/no-unnecessary-coercion
eslint-plugin\src\rules\no-redundant-type-constituents.ts
  172:10  error  Using !! on a boolean does not change the type or value of the boolean  @typescript-eslint/no-unnecessary-coercion
eslint-plugin\src\rules\no-shadow.ts
  90:47  error  Using !! on a boolean does not change the type or value of the boolean  @typescript-eslint/no-unnecessary-coercion
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
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
website\src\components\ast\HiddenItem.tsx
  45:14  error  Passing a string to String() does not change the type or value of the string  @typescript-eslint/no-unnecessary-coercion
website\src\components\config\ConfigEditor.tsx
  39:9  error  Passing a string to String() does not change the type or value of the string  @typescript-eslint/no-unnecessary-coercion
```
author_association: CONTRIBUTOR
user: login: kirkwaiblinger
created_at: 2024-06-15T16:05:46Z
body: @skondrashov Nice work btw 🙂. It's cool to see the number of spots within the typescript-eslint codebase that get fixed by your implementation (and IMO a good demo in favor of the rule). 
author_association: MEMBER
user: login: kirkwaiblinger
created_at: 2024-10-12T01:43:01Z
body: Noting that I think replacing explicit conversion/coercion with a `satisfies` expression would be a helpful suggestion to add to this rule (in addition to simply removing the conversion).
author_association: MEMBER
user: login: JoshuaKGoldberg
created_at: 2024-10-14T14:39:31Z
body: Yeah I'm also in favor of this. Each individual case (`Number()`, `!!`, ...) doesn't bring much value, but I'm constantly running into them in assorted projects. The fact that we had violations is great evidence, +1!

This might be the first time in recent memory (or ever?) an `evaluating community engagement` issue ended up getting a lot of 👍s and accepted.
author_association: MEMBER
user: login: kirkwaiblinger
created_at: 2024-10-14T20:13:32Z
body: @skondrashov If you'd like, feel free to send a PR with your implementation 🙂 
author_association: MEMBER
user: login: skondrashov
created_at: 2024-10-20T17:06:33Z
body: #10182

thanks for the warm feedback, appreciate it
author_association: CONTRIBUTOR

Diff:
diff --git a/eslint.config.mjs b/eslint.config.mjs
index a5abacc22809..88929e3bb2b4 100644
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -148,6 +148,7 @@ export default tseslint.config(
         'error',
         { allowConstantLoopConditions: true, checkTypePredicates: true },
       ],
+      '@typescript-eslint/no-unnecessary-type-conversion': 'error',
       '@typescript-eslint/no-unnecessary-type-parameters': 'error',
       '@typescript-eslint/no-unused-expressions': 'error',
       '@typescript-eslint/no-unused-vars': [
diff --git a/packages/eslint-plugin/docs/rules/no-unnecessary-type-conversion.mdx b/packages/eslint-plugin/docs/rules/no-unnecessary-type-conversion.mdx
new file mode 100644
index 000000000000..de0d7f1002bb
--- /dev/null
+++ b/packages/eslint-plugin/docs/rules/no-unnecessary-type-conversion.mdx
@@ -0,0 +1,79 @@
+---
+description: 'Disallow conversion idioms when they do not change the type or value of the expression.'
+---
+
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+
+> 🛑 This file is source code, not the primary documentation location! 🛑
+>
+> See **https://typescript-eslint.io/rules/no-unnecessary-type-conversion** for documentation.
+
+JavaScript provides several commonly used idioms to convert values to a specific type:
+
+- Primitive coercion (e.g. `Boolean(value)`, `String(value)`): using a built-in primitive function
+- String concatenation (e.g. `value + ''`): turning a value into a string
+- Unary coercion (e.g. `+value`, `!!value`): using a built-in operator
+- The `.toString()` method defined on many types
+
+These conversions are unnecessary if the value is already of that type.
+
+## Examples
+
+<Tabs>
+<TabItem value="❌ Incorrect">
+
+```ts
+String('123');
+'123'.toString();
+'' + '123';
+'123' + '';
+
+Number(123);
++123;
+~~123;
+
+Boolean(true);
+!!true;
+
+BigInt(BigInt(1));
+
+let str = '123';
+str += '';
+```
+
+</TabItem>
+<TabItem value="✅ Correct">
+
+```ts
+function foo(bar: string | number) {
+  String(bar);
+  bar.toString();
+  '' + bar;
+  bar + '';
+
+  Number(bar);
+  +bar;
+  ~~bar;
+
+  Boolean(bar);
+  !!bar;
+
+  BigInt(1);
+
+  bar += '';
+}
+```
+
+</TabItem>
+</Tabs>
+
+## When Not To Use It
+
+If you don't care about having no-op type conversions in your code, then you can turn off this rule.
+If you have types which are not accurate, then this rule might cause you to remove conversions that you actually do need.
+
+## Related To
+
+- [no-unnecessary-type-assertion](./no-unnecessary-type-assertion.mdx)
+- [no-useless-template-literals](./no-useless-template-literals.mdx)
diff --git a/packages/eslint-plugin/src/configs/eslintrc/all.ts b/packages/eslint-plugin/src/configs/eslintrc/all.ts
index 4a5b15ea9f43..3d25e79e33db 100644
--- a/packages/eslint-plugin/src/configs/eslintrc/all.ts
+++ b/packages/eslint-plugin/src/configs/eslintrc/all.ts
@@ -98,6 +98,7 @@ export = {
     '@typescript-eslint/no-unnecessary-type-arguments': 'error',
     '@typescript-eslint/no-unnecessary-type-assertion': 'error',
     '@typescript-eslint/no-unnecessary-type-constraint': 'error',
+    '@typescript-eslint/no-unnecessary-type-conversion': 'error',
     '@typescript-eslint/no-unnecessary-type-parameters': 'error',
     '@typescript-eslint/no-unsafe-argument': 'error',
     '@typescript-eslint/no-unsafe-assignment': 'error',
diff --git a/packages/eslint-plugin/src/configs/eslintrc/disable-type-checked.ts b/packages/eslint-plugin/src/configs/eslintrc/disable-type-checked.ts
index 2229d6715172..9dd6c95c929e 100644
--- a/packages/eslint-plugin/src/configs/eslintrc/disable-type-checked.ts
+++ b/packages/eslint-plugin/src/configs/eslintrc/disable-type-checked.ts
@@ -34,6 +34,7 @@ export = {
     '@typescript-eslint/no-unnecessary-template-expression': 'off',
     '@typescript-eslint/no-unnecessary-type-arguments': 'off',
     '@typescript-eslint/no-unnecessary-type-assertion': 'off',
+    '@typescript-eslint/no-unnecessary-type-conversion': 'off',
     '@typescript-eslint/no-unnecessary-type-parameters': 'off',
     '@typescript-eslint/no-unsafe-argument': 'off',
     '@typescript-eslint/no-unsafe-assignment': 'off',
diff --git a/packages/eslint-plugin/src/configs/flat/all.ts b/packages/eslint-plugin/src/configs/flat/all.ts
index 4bf1a3b4c5d4..dc40d391e4fd 100644
--- a/packages/eslint-plugin/src/configs/flat/all.ts
+++ b/packages/eslint-plugin/src/configs/flat/all.ts
@@ -112,6 +112,7 @@ export default (
       '@typescript-eslint/no-unnecessary-type-arguments': 'error',
       '@typescript-eslint/no-unnecessary-type-assertion': 'error',
       '@typescript-eslint/no-unnecessary-type-constraint': 'error',
+      '@typescript-eslint/no-unnecessary-type-conversion': 'error',
       '@typescript-eslint/no-unnecessary-type-parameters': 'error',
       '@typescript-eslint/no-unsafe-argument': 'error',
       '@typescript-eslint/no-unsafe-assignment': 'error',
diff --git a/packages/eslint-plugin/src/configs/flat/disable-type-checked.ts b/packages/eslint-plugin/src/configs/flat/disable-type-checked.ts
index 838f00e62c29..15c5bb0e3dcf 100644
--- a/packages/eslint-plugin/src/configs/flat/disable-type-checked.ts
+++ b/packages/eslint-plugin/src/configs/flat/disable-type-checked.ts
@@ -41,6 +41,7 @@ export default (
     '@typescript-eslint/no-unnecessary-template-expression': 'off',
     '@typescript-eslint/no-unnecessary-type-arguments': 'off',
     '@typescript-eslint/no-unnecessary-type-assertion': 'off',
+    '@typescript-eslint/no-unnecessary-type-conversion': 'off',
     '@typescript-eslint/no-unnecessary-type-parameters': 'off',
     '@typescript-eslint/no-unsafe-argument': 'off',
     '@typescript-eslint/no-unsafe-assignment': 'off',
diff --git a/packages/eslint-plugin/src/rules/adjacent-overload-signatures.ts b/packages/eslint-plugin/src/rules/adjacent-overload-signatures.ts
index 4729da05be7c..1c2b5ad1abaa 100644
--- a/packages/eslint-plugin/src/rules/adjacent-overload-signatures.ts
+++ b/packages/eslint-plugin/src/rules/adjacent-overload-signatures.ts
@@ -79,7 +79,7 @@ export default createRule({
           return {
             ...getNameFromMember(member, context.sourceCode),
             callSignature: false,
-            static: !!member.static,
+            static: member.static,
           };
         case AST_NODE_TYPES.TSCallSignatureDeclaration:
           return {
diff --git a/packages/eslint-plugin/src/rules/explicit-function-return-type.ts b/packages/eslint-plugin/src/rules/explicit-function-return-type.ts
index a8ee4aa3766b..8f1edc721685 100644
--- a/packages/eslint-plugin/src/rules/explicit-function-return-type.ts
+++ b/packages/eslint-plugin/src/rules/explicit-function-return-type.ts
@@ -169,14 +169,14 @@ export default createRule<Options, MessageIds>({
             }
           }
         }
-        if (!!funcName && !!options.allowedNames.includes(funcName)) {
+        if (!!funcName && options.allowedNames.includes(funcName)) {
           return true;
         }
       }
       if (
         node.type === AST_NODE_TYPES.FunctionDeclaration &&
         node.id &&
-        !!options.allowedNames.includes(node.id.name)
+        options.allowedNames.includes(node.id.name)
       ) {
         return true;
       }
diff --git a/packages/eslint-plugin/src/rules/index.ts b/packages/eslint-plugin/src/rules/index.ts
index f1587b5d2b1f..2a82c7e18c6b 100644
--- a/packages/eslint-plugin/src/rules/index.ts
+++ b/packages/eslint-plugin/src/rules/index.ts
@@ -75,6 +75,7 @@ import noUnnecessaryTemplateExpression from './no-unnecessary-template-expressio
 import noUnnecessaryTypeArguments from './no-unnecessary-type-arguments';
 import noUnnecessaryTypeAssertion from './no-unnecessary-type-assertion';
 import noUnnecessaryTypeConstraint from './no-unnecessary-type-constraint';
+import noUnnecessaryTypeConversion from './no-unnecessary-type-conversion';
 import noUnnecessaryTypeParameters from './no-unnecessary-type-parameters';
 import noUnsafeArgument from './no-unsafe-argument';
 import noUnsafeAssignment from './no-unsafe-assignment';
@@ -208,6 +209,7 @@ const rules = {
   'no-unnecessary-type-arguments': noUnnecessaryTypeArguments,
   'no-unnecessary-type-assertion': noUnnecessaryTypeAssertion,
   'no-unnecessary-type-constraint': noUnnecessaryTypeConstraint,
+  'no-unnecessary-type-conversion': noUnnecessaryTypeConversion,
   'no-unnecessary-type-parameters': noUnnecessaryTypeParameters,
   'no-unsafe-argument': noUnsafeArgument,
   'no-unsafe-assignment': noUnsafeAssignment,
diff --git a/packages/eslint-plugin/src/rules/member-ordering.ts b/packages/eslint-plugin/src/rules/member-ordering.ts
index 1f5dee70984c..7ae9ec7627cf 100644
--- a/packages/eslint-plugin/src/rules/member-ordering.ts
+++ b/packages/eslint-plugin/src/rules/member-ordering.ts
@@ -488,7 +488,7 @@ function isMemberOptional(node: Member): boolean {
     case AST_NODE_TYPES.PropertyDefinition:
     case AST_NODE_TYPES.TSAbstractMethodDefinition:
     case AST_NODE_TYPES.MethodDefinition:
-      return !!node.optional;
+      return node.optional;
   }
   return false;
 }
diff --git a/packages/eslint-plugin/src/rules/no-duplicate-enum-values.ts b/packages/eslint-plugin/src/rules/no-duplicate-enum-values.ts
index 40a0ccee82b3..c5a858dbdcaa 100644
--- a/packages/eslint-plugin/src/rules/no-duplicate-enum-values.ts
+++ b/packages/eslint-plugin/src/rules/no-duplicate-enum-values.ts
@@ -58,9 +58,9 @@ export default createRule({
 
           let value: number | string | undefined;
           if (isStringLiteral(member.initializer)) {
-            value = String(member.initializer.value);
+            value = member.initializer.value;
           } else if (isNumberLiteral(member.initializer)) {
-            value = Number(member.initializer.value);
+            value = member.initializer.value;
           } else if (isStaticTemplateLiteral(member.initializer)) {
             value = member.initializer.quasis[0].value.cooked;
           }
diff --git a/packages/eslint-plugin/src/rules/no-empty-interface.ts b/packages/eslint-plugin/src/rules/no-empty-interface.ts
index cbcb38f1092c..4330f27e0acf 100644
--- a/packages/eslint-plugin/src/rules/no-empty-interface.ts
+++ b/packages/eslint-plugin/src/rules/no-empty-interface.ts
@@ -97,11 +97,10 @@ export default createRule<Options, MessageIds>({
               def => def.node.type === AST_NODE_TYPES.ClassDeclaration,
             );
 
-          const isInAmbientDeclaration = !!(
+          const isInAmbientDeclaration =
             isDefinitionFile(context.filename) &&
             scope.type === ScopeType.tsModule &&
-            scope.block.declare
-          );
+            scope.block.declare;
 
           const useAutoFix = !(
             isInAmbientDeclaration || mergedWithClassDeclaration
diff --git a/packages/eslint-plugin/src/rules/no-redundant-type-constituents.ts b/packages/eslint-plugin/src/rules/no-redundant-type-constituents.ts
index b550802df58d..acf73ce9326a 100644
--- a/packages/eslint-plugin/src/rules/no-redundant-type-constituents.ts
+++ b/packages/eslint-plugin/src/rules/no-redundant-type-constituents.ts
@@ -172,7 +172,7 @@ function describeLiteralTypeNode(typeNode: TSESTree.TypeNode): string {
 }
 
 function isNodeInsideReturnType(node: TSESTree.TSUnionType): boolean {
-  return !!(
+  return (
     node.parent.type === AST_NODE_TYPES.TSTypeAnnotation &&
     isFunctionOrFunctionType(node.parent.parent)
   );
diff --git a/packages/eslint-plugin/src/rules/no-unnecessary-template-expression.ts b/packages/eslint-plugin/src/rules/no-unnecessary-template-expression.ts
index 6c70e134043e..c056e1af1a07 100644
--- a/packages/eslint-plugin/src/rules/no-unnecessary-template-expression.ts
+++ b/packages/eslint-plugin/src/rules/no-unnecessary-template-expression.ts
@@ -35,9 +35,7 @@ const evenNumOfBackslashesRegExp = /(?<!(?:[^\\]|^)(?:\\\\)*\\)/;
 // '\\\\$' <- true
 // '\\\\\\$' <- false
 function endsWithUnescapedDollarSign(str: string): boolean {
-  return new RegExp(`${String(evenNumOfBackslashesRegExp.source)}\\$$`).test(
-    str,
-  );
+  return new RegExp(`${evenNumOfBackslashesRegExp.source}\\$$`).test(str);
 }
 
 export default createRule<[], MessageId>({
@@ -316,10 +314,7 @@ export default createRule<[], MessageId>({
             // \${ -> \${
             // \\${ -> \\\${
             .replaceAll(
-              new RegExp(
-                `${String(evenNumOfBackslashesRegExp.source)}(\`|\\\${)`,
-                'g',
-              ),
+              new RegExp(`${evenNumOfBackslashesRegExp.source}(\`|\\\${)`, 'g'),
               '\\$1',
             );
 
diff --git a/packages/eslint-plugin/src/rules/no-unnecessary-type-conversion.ts b/packages/eslint-plugin/src/rules/no-unnecessary-type-conversion.ts
new file mode 100644
index 000000000000..dfa4a57de456
--- /dev/null
+++ b/packages/eslint-plugin/src/rules/no-unnecessary-type-conversion.ts
@@ -0,0 +1,362 @@
+import type { TSESTree } from '@typescript-eslint/utils';
+import type { RuleFix, RuleFixer } from '@typescript-eslint/utils/ts-eslint';
+
+import { AST_NODE_TYPES } from '@typescript-eslint/utils';
+import { unionTypeParts } from 'ts-api-utils';
+import * as ts from 'typescript';
+
+import {
+  createRule,
+  getConstrainedTypeAtLocation,
+  getParserServices,
+  getWrappingFixer,
+  isTypeFlagSet,
+} from '../util';
+
+type Options = [];
+type MessageIds =
+  | 'suggestRemove'
+  | 'suggestSatisfies'
+  | 'unnecessaryTypeConversion';
+
+export default createRule<Options, MessageIds>({
+  name: 'no-unnecessary-type-conversion',
+  meta: {
+    type: 'suggestion',
+    docs: {
+      description:
+        'Disallow conversion idioms when they do not change the type or value of the expression',
+      requiresTypeChecking: true,
+    },
+    fixable: 'code',
+    hasSuggestions: true,
+    messages: {
+      suggestRemove: 'Remove the type conversion.',
+      suggestSatisfies:
+        'Instead, assert that the value satisfies the {{type}} type.',
+      unnecessaryTypeConversion:
+        '{{violation}} does not change the type or value of the {{type}}.',
+    },
+    schema: [],
+  },
+  defaultOptions: [],
+  create(context) {
+    function doesUnderlyingTypeMatchFlag(
+      type: ts.Type,
+      typeFlag: ts.TypeFlags,
+    ): boolean {
+      return unionTypeParts(type).every(t => isTypeFlagSet(t, typeFlag));
+    }
+
+    const services = getParserServices(context);
+
+    function handleUnaryOperator(
+      node: TSESTree.UnaryExpression,
+      typeFlag: ts.TypeFlags,
+      typeString: 'boolean' | 'number',
+      violation: string,
+      isDoubleOperator: boolean, // !! or ~~
+    ) {
+      const outerNode = isDoubleOperator ? node.parent : node;
+      const type = services.getTypeAtLocation(node.argument);
+      if (doesUnderlyingTypeMatchFlag(type, typeFlag)) {
+        const wrappingFixerParams = {
+          node: outerNode,
+          innerNode: [node.argument],
+          sourceCode: context.sourceCode,
+        };
+
+        context.report({
+          loc: {
+            start: outerNode.loc.start,
+            end: {
+              column: node.loc.start.column + 1,
+              line: node.loc.start.line,
+            },
+          },
+          messageId: 'unnecessaryTypeConversion',
+          data: { type: typeString, violation },
+          suggest: [
+            {
+              messageId: 'suggestRemove',
+              fix: getWrappingFixer(wrappingFixerParams),
+            },
+            {
+              messageId: 'suggestSatisfies',
+              data: { type: typeString },
+              fix: getWrappingFixer({
+                ...wrappingFixerParams,
+                wrap: expr => `${expr} satisfies ${typeString}`,
+              }),
+            },
+          ],
+        });
+      }
+    }
+
+    return {
+      'AssignmentExpression[operator = "+="]'(
+        node: TSESTree.AssignmentExpression,
+      ): void {
+        if (
+          node.right.type === AST_NODE_TYPES.Literal &&
+          node.right.value === '' &&
+          doesUnderlyingTypeMatchFlag(
+            services.getTypeAtLocation(node.left),
+            ts.TypeFlags.StringLike,
+          )
+        ) {
+          const wrappingFixerParams = {
+            node,
+            innerNode: [node.left],
+            sourceCode: context.sourceCode,
+          };
+
+          context.report({
+            node,
+            messageId: 'unnecessaryTypeConversion',
+            data: {
+              type: 'string',
+              violation: "Concatenating a string with ''",
+            },
+            suggest: [
+              {
+                messageId: 'suggestRemove',
+                fix:
+                  node.parent.type === AST_NODE_TYPES.ExpressionStatement
+                    ? (fixer: RuleFixer): RuleFix[] => [
+                        fixer.removeRange([
+                          node.parent.range[0],
+                          node.parent.range[1],
+                        ]),
+                      ]
+                    : getWrappingFixer(wrappingFixerParams),
+              },
+              {
+                messageId: 'suggestSatisfies',
+                data: { type: 'string' },
+                fix: getWrappingFixer({
+                  ...wrappingFixerParams,
+                  wrap: expr => `${expr} satisfies string`,
+                }),
+              },
+            ],
+          });
+        }
+      },
+      'BinaryExpression[operator = "+"]'(
+        node: TSESTree.BinaryExpression,
+      ): void {
+        if (
+          node.right.type === AST_NODE_TYPES.Literal &&
+          node.right.value === '' &&
+          doesUnderlyingTypeMatchFlag(
+            services.getTypeAtLocation(node.left),
+            ts.TypeFlags.StringLike,
+          )
+        ) {
+          const wrappingFixerParams = {
+            node,
+            innerNode: [node.left],
+            sourceCode: context.sourceCode,
+          };
+
+          context.report({
+            loc: {
+              start: node.left.loc.end,
+              end: node.loc.end,
+            },
+            messageId: 'unnecessaryTypeConversion',
+            data: {
+              type: 'string',
+              violation: "Concatenating a string with ''",
+            },
+            suggest: [
+              {
+                messageId: 'suggestRemove',
+                fix: getWrappingFixer(wrappingFixerParams),
+              },
+              {
+                messageId: 'suggestSatisfies',
+                data: { type: 'string' },
+                fix: getWrappingFixer({
+                  ...wrappingFixerParams,
+                  wrap: expr => `${expr} satisfies string`,
+                }),
+              },
+            ],
+          });
+        } else if (
+          node.left.type === AST_NODE_TYPES.Literal &&
+          node.left.value === '' &&
+          doesUnderlyingTypeMatchFlag(
+            services.getTypeAtLocation(node.right),
+            ts.TypeFlags.StringLike,
+          )
+        ) {
+          const wrappingFixerParams = {
+            node,
+            innerNode: [node.right],
+            sourceCode: context.sourceCode,
+          };
+
+          context.report({
+            loc: {
+              start: node.loc.start,
+              end: node.right.loc.start,
+            },
+            messageId: 'unnecessaryTypeConversion',
+            data: {
+              type: 'string',
+              violation: "Concatenating '' with a string",
+            },
+            suggest: [
+              {
+                messageId: 'suggestRemove',
+                fix: getWrappingFixer(wrappingFixerParams),
+              },
+              {
+                messageId: 'suggestSatisfies',
+                data: { type: 'string' },
+                fix: getWrappingFixer({
+                  ...wrappingFixerParams,
+                  wrap: expr => `${expr} satisfies string`,
+                }),
+              },
+            ],
+          });
+        }
+      },
+      CallExpression(node: TSESTree.CallExpression): void {
+        const nodeCallee = node.callee;
+        const builtInTypeFlags = {
+          BigInt: ts.TypeFlags.BigIntLike,
+          Boolean: ts.TypeFlags.BooleanLike,
+          Number: ts.TypeFlags.NumberLike,
+          String: ts.TypeFlags.StringLike,
+        };
+
+        if (
+          nodeCallee.type !== AST_NODE_TYPES.Identifier ||
+          !(nodeCallee.name in builtInTypeFlags)
+        ) {
+          return;
+        }
+
+        const typeFlag =
+          builtInTypeFlags[nodeCallee.name as keyof typeof builtInTypeFlags];
+        const scope = context.sourceCode.getScope(node);
+        const variable = scope.set.get(nodeCallee.name);
+        if (
+          !!variable?.defs.length ||
+          !doesUnderlyingTypeMatchFlag(
+            getConstrainedTypeAtLocation(services, node.arguments[0]),
+            typeFlag,
+          )
+        ) {
+          return;
+        }
+
+        const wrappingFixerParams = {
+          node,
+          innerNode: [node.arguments[0]],
+          sourceCode: context.sourceCode,
+        };
+        const typeString = nodeCallee.name.toLowerCase();
+
+        context.report({
+          node: nodeCallee,
+          messageId: 'unnecessaryTypeConversion',
+          data: {
+            type: nodeCallee.name.toLowerCase(),
+            violation: `Passing a ${typeString} to ${nodeCallee.name}()`,
+          },
+          suggest: [
+            {
+              messageId: 'suggestRemove',
+              fix: getWrappingFixer(wrappingFixerParams),
+            },
+            {
+              messageId: 'suggestSatisfies',
+              data: { type: typeString },
+              fix: getWrappingFixer({
+                ...wrappingFixerParams,
+                wrap: expr => `${expr} satisfies ${typeString}`,
+              }),
+            },
+          ],
+        });
+      },
+      'CallExpression > MemberExpression.callee > Identifier[name = "toString"].property'(
+        node: TSESTree.Expression,
+      ): void {
+        const memberExpr = node.parent as TSESTree.MemberExpression;
+        const type = getConstrainedTypeAtLocation(services, memberExpr.object);
+        if (doesUnderlyingTypeMatchFlag(type, ts.TypeFlags.StringLike)) {
+          const wrappingFixerParams = {
+            node: memberExpr.parent,
+            innerNode: [memberExpr.object],
+            sourceCode: context.sourceCode,
+          };
+
+          context.report({
+            loc: {
+              start: memberExpr.property.loc.start,
+              end: memberExpr.parent.loc.end,
+            },
+            messageId: 'unnecessaryTypeConversion',
+            data: {
+              type: 'string',
+              violation: "Calling a string's .toString() method",
+            },
+            suggest: [
+              {
+                messageId: 'suggestRemove',
+                fix: getWrappingFixer(wrappingFixerParams),
+              },
+              {
+                messageId: 'suggestSatisfies',
+                data: { type: 'string' },
+                fix: getWrappingFixer({
+                  ...wrappingFixerParams,
+                  wrap: expr => `${expr} satisfies string`,
+                }),
+              },
+            ],
+          });
+        }
+      },
+      'UnaryExpression[operator = "!"] > UnaryExpression[operator = "!"]'(
+        node: TSESTree.UnaryExpression,
+      ): void {
+        handleUnaryOperator(
+          node,
+          ts.TypeFlags.BooleanLike,
+          'boolean',
+          'Using !! on a boolean',
+          true,
+        );
+      },
+      'UnaryExpression[operator = "+"]'(node: TSESTree.UnaryExpression): void {
+        handleUnaryOperator(
+          node,
+          ts.TypeFlags.NumberLike,
+          'number',
+          'Using the unary + operator on a number',
+          false,
+        );
+      },
+      'UnaryExpression[operator = "~"] > UnaryExpression[operator = "~"]'(
+        node: TSESTree.UnaryExpression,
+      ): void {
+        handleUnaryOperator(
+          node,
+          ts.TypeFlags.NumberLike,
+          'number',
+          'Using ~~ on a number',
+          true,
+        );
+      },
+    };
+  },
+});
diff --git a/packages/eslint-plugin/src/rules/no-unsafe-assignment.ts b/packages/eslint-plugin/src/rules/no-unsafe-assignment.ts
index d0b31a5e8446..2dd9ca6b5d2e 100644
--- a/packages/eslint-plugin/src/rules/no-unsafe-assignment.ts
+++ b/packages/eslint-plugin/src/rules/no-unsafe-assignment.ts
@@ -200,7 +200,7 @@ export default createRule({
           receiverProperty.key.type === AST_NODE_TYPES.TemplateLiteral &&
           receiverProperty.key.quasis.length === 1
         ) {
-          key = String(receiverProperty.key.quasis[0].value.cooked);
+          key = receiverProperty.key.quasis[0].value.cooked;
         } else {
           // can't figure out the name, so skip it
           continue;
diff --git a/packages/eslint-plugin/src/rules/prefer-return-this-type.ts b/packages/eslint-plugin/src/rules/prefer-return-this-type.ts
index fff92141fe12..7d7b14044cfb 100644
--- a/packages/eslint-plugin/src/rules/prefer-return-this-type.ts
+++ b/packages/eslint-plugin/src/rules/prefer-return-this-type.ts
@@ -62,7 +62,7 @@ export default createRule({
 
     function isThisSpecifiedInParameters(originalFunc: FunctionLike): boolean {
       const firstArg = originalFunc.params.at(0);
-      return !!(
+      return (
         firstArg?.type === AST_NODE_TYPES.Identifier && firstArg.name === 'this'
       );
     }
diff --git a/packages/eslint-plugin/src/util/getWrappingFixer.ts b/packages/eslint-plugin/src/util/getWrappingFixer.ts
index 2ad20e1d4893..6431b5955040 100644
--- a/packages/eslint-plugin/src/util/getWrappingFixer.ts
+++ b/packages/eslint-plugin/src/util/getWrappingFixer.ts
@@ -23,11 +23,11 @@ interface WrappingFixerParams {
    * Receives multiple arguments if there are multiple innerNodes.
    * E.g. ``code => `${code} != null` ``
    */
-  wrap: (...code: string[]) => string;
+  wrap?: (...code: string[]) => string;
 }
 
 /**
- * Wraps node with some code. Adds parenthesis as necessary.
+ * Wraps node with some code. Adds parentheses as necessary.
  * @returns Fixer which adds the specified code and parens if necessary.
  */
 export function getWrappingFixer(
@@ -55,6 +55,10 @@ export function getWrappingFixer(
       return code;
     });
 
+    if (!wrap) {
+      return fixer.replaceText(node, innerCodes.join(''));
+    }
+
     // do the wrapping
     let code = wrap(...innerCodes);
 
@@ -105,7 +109,7 @@ export function getMovedNodeCode(params: {
 }
 
 /**
- * Check if a node will always have the same precedence if it's parent changes.
+ * Check if a node will always have the same precedence if its parent changes.
  */
 export function isStrongPrecedenceNode(innerNode: TSESTree.Node): boolean {
   return (
diff --git a/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-unnecessary-type-conversion.shot b/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-unnecessary-type-conversion.shot
new file mode 100644
index 000000000000..1231d273a43f
--- /dev/null
+++ b/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-unnecessary-type-conversion.shot
@@ -0,0 +1,49 @@
+Incorrect
+
+String('123');
+~~~~~~ Passing a string to String() does not change the type or value of the string.
+'123'.toString();
+      ~~~~~~~~~~ Calling a string's .toString() method does not change the type or value of the string.
+'' + '123';
+~~~~~ Concatenating '' with a string does not change the type or value of the string.
+'123' + '';
+     ~~~~~ Concatenating a string with '' does not change the type or value of the string.
+
+Number(123);
+~~~~~~ Passing a number to Number() does not change the type or value of the number.
++123;
+~ Using the unary + operator on a number does not change the type or value of the number.
+~~123;
+~~ Using ~~ on a number does not change the type or value of the number.
+
+Boolean(true);
+~~~~~~~ Passing a boolean to Boolean() does not change the type or value of the boolean.
+!!true;
+~~ Using !! on a boolean does not change the type or value of the boolean.
+
+BigInt(BigInt(1));
+~~~~~~ Passing a bigint to BigInt() does not change the type or value of the bigint.
+
+let str = '123';
+str += '';
+~~~~~~~~~ Concatenating a string with '' does not change the type or value of the string.
+
+Correct
+
+function foo(bar: string | number) {
+  String(bar);
+  bar.toString();
+  '' + bar;
+  bar + '';
+
+  Number(bar);
+  +bar;
+  ~~bar;
+
+  Boolean(bar);
+  !!bar;
+
+  BigInt(1);
+
+  bar += '';
+}
diff --git a/packages/eslint-plugin/tests/rules/await-thenable.test.ts b/packages/eslint-plugin/tests/rules/await-thenable.test.ts
index 1860db956c3e..e64f0a5f1f3c 100644
--- a/packages/eslint-plugin/tests/rules/await-thenable.test.ts
+++ b/packages/eslint-plugin/tests/rules/await-thenable.test.ts
@@ -4,7 +4,6 @@ import rule from '../../src/rules/await-thenable';
 import { getFixturesRootDir } from '../RuleTester';
 
 const rootDir = getFixturesRootDir();
-const messageId = 'await';
 
 const ruleTester = new RuleTester({
   languageOptions: {
@@ -348,7 +347,7 @@ class C<R extends unknown> {
       errors: [
         {
           line: 1,
-          messageId,
+          messageId: 'await',
           suggestions: [
             {
               messageId: 'removeAwait',
@@ -363,7 +362,7 @@ class C<R extends unknown> {
       errors: [
         {
           line: 1,
-          messageId,
+          messageId: 'await',
           suggestions: [
             {
               messageId: 'removeAwait',
@@ -378,7 +377,7 @@ class C<R extends unknown> {
       errors: [
         {
           line: 1,
-          messageId,
+          messageId: 'await',
           suggestions: [
             {
               messageId: 'removeAwait',
@@ -393,7 +392,7 @@ class C<R extends unknown> {
       errors: [
         {
           line: 1,
-          messageId,
+          messageId: 'await',
           suggestions: [
             {
               messageId: 'removeAwait',
@@ -411,7 +410,7 @@ await new NonPromise();
       errors: [
         {
           line: 3,
-          messageId,
+          messageId: 'await',
           suggestions: [
             {
               messageId: 'removeAwait',
@@ -438,7 +437,7 @@ async function test() {
       errors: [
         {
           line: 8,
-          messageId,
+          messageId: 'await',
           suggestions: [
             {
               messageId: 'removeAwait',
@@ -465,7 +464,7 @@ await callback?.();
       errors: [
         {
           line: 3,
-          messageId,
+          messageId: 'await',
           suggestions: [
             {
               messageId: 'removeAwait',
@@ -486,7 +485,7 @@ await obj.a?.b?.();
       errors: [
         {
           line: 3,
-          messageId,
+          messageId: 'await',
           suggestions: [
             {
               messageId: 'removeAwait',
@@ -507,7 +506,7 @@ await obj?.a.b.c?.();
       errors: [
         {
           line: 3,
-          messageId,
+          messageId: 'await',
           suggestions: [
             {
               messageId: 'removeAwait',
diff --git a/packages/eslint-plugin/tests/rules/no-array-constructor.test.ts b/packages/eslint-plugin/tests/rules/no-array-constructor.test.ts
index bbba24e7b75c..ce97e7e6a8a2 100644
--- a/packages/eslint-plugin/tests/rules/no-array-constructor.test.ts
+++ b/packages/eslint-plugin/tests/rules/no-array-constructor.test.ts
@@ -5,8 +5,6 @@ import rule from '../../src/rules/no-array-constructor';
 
 const ruleTester = new RuleTester();
 
-const messageId = 'useLiteral';
-
 ruleTester.run('no-array-constructor', rule, {
   valid: [
     'new Array(x);',
@@ -40,7 +38,7 @@ ruleTester.run('no-array-constructor', rule, {
       code: 'new Array();',
       errors: [
         {
-          messageId,
+          messageId: 'useLiteral',
           type: AST_NODE_TYPES.NewExpression,
         },
       ],
@@ -50,7 +48,7 @@ ruleTester.run('no-array-constructor', rule, {
       code: 'Array();',
       errors: [
         {
-          messageId,
+          messageId: 'useLiteral',
           type: AST_NODE_TYPES.CallExpression,
         },
       ],
@@ -60,7 +58,7 @@ ruleTester.run('no-array-constructor', rule, {
       code: 'Array?.();',
       errors: [
         {
-          messageId,
+          messageId: 'useLiteral',
           type: AST_NODE_TYPES.CallExpression,
         },
       ],
@@ -70,7 +68,7 @@ ruleTester.run('no-array-constructor', rule, {
       code: '/* a */ /* b */ Array /* c */ /* d */ /* e */ /* f */?.(); /* g */ /* h */',
       errors: [
         {
-          messageId,
+          messageId: 'useLiteral',
           type: AST_NODE_TYPES.CallExpression,
         },
       ],
@@ -80,7 +78,7 @@ ruleTester.run('no-array-constructor', rule, {
       code: 'new Array(x, y);',
       errors: [
         {
-          messageId,
+          messageId: 'useLiteral',
           type: AST_NODE_TYPES.NewExpression,
         },
       ],
@@ -90,7 +88,7 @@ ruleTester.run('no-array-constructor', rule, {
       code: 'Array(x, y);',
       errors: [
         {
-          messageId,
+          messageId: 'useLiteral',
           type: AST_NODE_TYPES.CallExpression,
         },
       ],
@@ -100,7 +98,7 @@ ruleTester.run('no-array-constructor', rule, {
       code: 'Array?.(x, y);',
       errors: [
         {
-          messageId,
+          messageId: 'useLiteral',
           type: AST_NODE_TYPES.CallExpression,
         },
       ],
@@ -110,7 +108,7 @@ ruleTester.run('no-array-constructor', rule, {
       code: '/* a */ /* b */ Array /* c */ /* d */ /* e */ /* f */?.(x, y); /* g */ /* h */',
       errors: [
         {
-          messageId,
+          messageId: 'useLiteral',
           type: AST_NODE_TYPES.CallExpression,
         },
       ],
@@ -120,7 +118,7 @@ ruleTester.run('no-array-constructor', rule, {
       code: 'new Array(0, 1, 2);',
       errors: [
         {
-          messageId,
+          messageId: 'useLiteral',
           type: AST_NODE_TYPES.NewExpression,
         },
       ],
@@ -130,7 +128,7 @@ ruleTester.run('no-array-constructor', rule, {
       code: 'Array(0, 1, 2);',
       errors: [
         {
-          messageId,
+          messageId: 'useLiteral',
           type: AST_NODE_TYPES.CallExpression,
         },
       ],
@@ -140,7 +138,7 @@ ruleTester.run('no-array-constructor', rule, {
       code: 'Array?.(0, 1, 2);',
       errors: [
         {
-          messageId,
+          messageId: 'useLiteral',
           type: AST_NODE_TYPES.CallExpression,
         },
       ],
@@ -156,7 +154,7 @@ ruleTester.run('no-array-constructor', rule, {
       `,
       errors: [
         {
-          messageId,
+          messageId: 'useLiteral',
           type: AST_NODE_TYPES.CallExpression,
         },
       ],
@@ -174,7 +172,7 @@ new Array(0, 1, 2);
       `,
       errors: [
         {
-          messageId,
+          messageId: 'useLiteral',
           type: AST_NODE_TYPES.NewExpression,
         },
       ],
diff --git a/packages/eslint-plugin/tests/rules/no-unnecessary-qualifier.test.ts b/packages/eslint-plugin/tests/rules/no-unnecessary-qualifier.test.ts
index 80ba657442a2..91a153d7dd33 100644
--- a/packages/eslint-plugin/tests/rules/no-unnecessary-qualifier.test.ts
+++ b/packages/eslint-plugin/tests/rules/no-unnecessary-qualifier.test.ts
@@ -15,8 +15,6 @@ const ruleTester = new RuleTester({
   },
 });
 
-const messageId = 'unnecessaryQualifier';
-
 ruleTester.run('no-unnecessary-qualifier', rule, {
   valid: [
     `
@@ -95,7 +93,7 @@ namespace A {
       `,
       errors: [
         {
-          messageId,
+          messageId: 'unnecessaryQualifier',
           type: AST_NODE_TYPES.Identifier,
         },
       ],
@@ -115,7 +113,7 @@ namespace A {
       `,
       errors: [
         {
-          messageId,
+          messageId: 'unnecessaryQualifier',
           type: AST_NODE_TYPES.Identifier,
         },
       ],
@@ -137,7 +135,7 @@ namespace A {
       `,
       errors: [
         {
-          messageId,
+          messageId: 'unnecessaryQualifier',
           type: AST_NODE_TYPES.Identifier,
         },
       ],
@@ -161,7 +159,7 @@ namespace A {
       `,
       errors: [
         {
-          messageId,
+          messageId: 'unnecessaryQualifier',
           type: AST_NODE_TYPES.TSQualifiedName,
         },
       ],
@@ -185,7 +183,7 @@ namespace A {
       `,
       errors: [
         {
-          messageId,
+          messageId: 'unnecessaryQualifier',
           type: AST_NODE_TYPES.TSQualifiedName,
         },
       ],
@@ -209,7 +207,7 @@ namespace A {
       `,
       errors: [
         {
-          messageId,
+          messageId: 'unnecessaryQualifier',
           type: AST_NODE_TYPES.MemberExpression,
         },
       ],
@@ -231,7 +229,7 @@ enum A {
       `,
       errors: [
         {
-          messageId,
+          messageId: 'unnecessaryQualifier',
           type: AST_NODE_TYPES.Identifier,
         },
       ],
@@ -253,7 +251,7 @@ namespace Foo {
       `,
       errors: [
         {
-          messageId,
+          messageId: 'unnecessaryQualifier',
           type: AST_NODE_TYPES.MemberExpression,
         },
       ],
@@ -275,7 +273,7 @@ declare module './foo' {
       `,
       errors: [
         {
-          messageId,
+          messageId: 'unnecessaryQualifier',
           type: AST_NODE_TYPES.Identifier,
         },
       ],
diff --git a/packages/eslint-plugin/tests/rules/no-unnecessary-type-conversion.test.ts b/packages/eslint-plugin/tests/rules/no-unnecessary-type-conversion.test.ts
new file mode 100644
index 000000000000..e0aaadacee2d
--- /dev/null
+++ b/packages/eslint-plugin/tests/rules/no-unnecessary-type-conversion.test.ts
@@ -0,0 +1,724 @@
+import { RuleTester } from '@typescript-eslint/rule-tester';
+
+import rule from '../../src/rules/no-unnecessary-type-conversion';
+import { getFixturesRootDir } from '../RuleTester';
+
+const rootDir = getFixturesRootDir();
+
+const ruleTester = new RuleTester({
+  languageOptions: {
+    parserOptions: {
+      project: './tsconfig.json',
+      tsconfigRootDir: rootDir,
+    },
+  },
+});
+
+ruleTester.run('no-unnecessary-type-conversion', rule, {
+  valid: [
+    // standard type conversions are valid
+    'String(1);',
+    '(1).toString();',
+    '`${1}`;',
+    "'' + 1;",
+    "1 + '';",
+    `
+      let str = 1;
+      str += '';
+    `,
+    "Number('2');",
+    "+'2';",
+    "~~'2';",
+    'Boolean(0);',
+    '!!0;',
+    'BigInt(3);',
+
+    // things that are not type conversion idioms (but look similar) are valid
+    "new String('asdf');",
+    'new Number(2);',
+    'new Boolean(true);',
+    '!false;',
+    '~2;',
+    `
+      function String(value: unknown) {
+        return value;
+      }
+      String('asdf');
+      export {};
+    `,
+    `
+      function Number(value: unknown) {
+        return value;
+      }
+      Number(2);
+      export {};
+    `,
+    `
+      function Boolean(value: unknown) {
+        return value;
+      }
+      Boolean(true);
+      export {};
+    `,
+    `
+      function BigInt(value: unknown) {
+        return value;
+      }
+      BigInt(3n);
+      export {};
+    `,
+    `
+      function toString(value: unknown) {
+        return value;
+      }
+      toString('asdf');
+    `,
+    `
+      export {};
+      declare const toString: string;
+      toString.toUpperCase();
+    `,
+
+    // using type conversion idioms to unbox boxed primitives is valid
+    'String(new String());',
+    'new String().toString();',
+    "'' + new String();",
+    "new String() + '';",
+    `
+      let str = new String();
+      str += '';
+    `,
+    'Number(new Number());',
+    '+new Number();',
+    '~~new Number();',
+    'Boolean(new Boolean());',
+    '!!new Boolean();',
+  ],
+
+  invalid: [
+    {
+      code: "String('asdf');",
+      errors: [
+        {
+          column: 1,
+          endColumn: 7,
+          messageId: 'unnecessaryTypeConversion',
+          suggestions: [
+            {
+              messageId: 'suggestRemove',
+              output: "'asdf';",
+            },
+            {
+              messageId: 'suggestSatisfies',
+              output: "'asdf' satisfies string;",
+            },
+          ],
+        },
+      ],
+    },
+    {
+      code: "'asdf'.toString();",
+      errors: [
+        {
+          column: 8,
+          endColumn: 18,
+          messageId: 'unnecessaryTypeConversion',
+          suggestions: [
+            {
+              messageId: 'suggestRemove',
+              output: "'asdf';",
+            },
+            {
+              messageId: 'suggestSatisfies',
+              output: "'asdf' satisfies string;",
+            },
+          ],
+        },
+      ],
+    },
+    {
+      code: "'' + 'asdf';",
+      errors: [
+        {
+          column: 1,
+          endColumn: 6,
+          messageId: 'unnecessaryTypeConversion',
+          suggestions: [
+            {
+              messageId: 'suggestRemove',
+              output: "'asdf';",
+            },
+            {
+              messageId: 'suggestSatisfies',
+              output: "'asdf' satisfies string;",
+            },
+          ],
+        },
+      ],
+    },
+    {
+      code: "'asdf' + '';",
+      errors: [
+        {
+          column: 7,
+          endColumn: 12,
+          messageId: 'unnecessaryTypeConversion',
+          suggestions: [
+            {
+              messageId: 'suggestRemove',
+              output: "'asdf';",
+            },
+            {
+              messageId: 'suggestSatisfies',
+              output: "'asdf' satisfies string;",
+            },
+          ],
+        },
+      ],
+    },
+    {
+      code: `
+let str = 'asdf';
+str += '';
+      `,
+      errors: [
+        {
+          column: 1,
+          endColumn: 10,
+          endLine: 3,
+          line: 3,
+          messageId: 'unnecessaryTypeConversion',
+          suggestions: [
+            {
+              messageId: 'suggestRemove',
+              output: `
+let str = 'asdf';
+
+      `,
+            },
+            {
+              messageId: 'suggestSatisfies',
+              output: `
+let str = 'asdf';
+str satisfies string;
+      `,
+            },
+          ],
+        },
+      ],
+    },
+    {
+      code: `
+let str = 'asdf';
+'asdf' + (str += '');
+      `,
+      errors: [
+        {
+          column: 11,
+          endColumn: 20,
+          endLine: 3,
+          line: 3,
+          messageId: 'unnecessaryTypeConversion',
+          suggestions: [
+            {
+              messageId: 'suggestRemove',
+              output: `
+let str = 'asdf';
+'asdf' + (str);
+      `,
+            },
+            {
+              messageId: 'suggestSatisfies',
+              output: `
+let str = 'asdf';
+'asdf' + (str satisfies string);
+      `,
+            },
+          ],
+        },
+      ],
+    },
+    {
+      code: 'Number(123);',
+      errors: [
+        {
+          column: 1,
+          endColumn: 7,
+          messageId: 'unnecessaryTypeConversion',
+          suggestions: [
+            {
+              messageId: 'suggestRemove',
+              output: '123;',
+            },
+            {
+              messageId: 'suggestSatisfies',
+              output: '123 satisfies number;',
+            },
+          ],
+        },
+      ],
+    },
+    {
+      code: '+123;',
+      errors: [
+        {
+          column: 1,
+          endColumn: 2,
+          messageId: 'unnecessaryTypeConversion',
+          suggestions: [
+            {
+              messageId: 'suggestRemove',
+              output: '123;',
+            },
+            {
+              messageId: 'suggestSatisfies',
+              output: '123 satisfies number;',
+            },
+          ],
+        },
+      ],
+    },
+    {
+      code: '~~123;',
+      errors: [
+        {
+          column: 1,
+          endColumn: 3,
+          messageId: 'unnecessaryTypeConversion',
+          suggestions: [
+            {
+              messageId: 'suggestRemove',
+              output: '123;',
+            },
+            {
+              messageId: 'suggestSatisfies',
+              output: '123 satisfies number;',
+            },
+          ],
+        },
+      ],
+    },
+    {
+      code: 'Boolean(true);',
+      errors: [
+        {
+          column: 1,
+          endColumn: 8,
+          messageId: 'unnecessaryTypeConversion',
+          suggestions: [
+            {
+              messageId: 'suggestRemove',
+              output: 'true;',
+            },
+            {
+              messageId: 'suggestSatisfies',
+              output: 'true satisfies boolean;',
+            },
+          ],
+        },
+      ],
+    },
+    {
+      code: '!!true;',
+      errors: [
+        {
+          column: 1,
+          endColumn: 3,
+          messageId: 'unnecessaryTypeConversion',
+          suggestions: [
+            {
+              messageId: 'suggestRemove',
+              output: 'true;',
+            },
+            {
+              messageId: 'suggestSatisfies',
+              output: 'true satisfies boolean;',
+            },
+          ],
+        },
+      ],
+    },
+    {
+      code: 'BigInt(3n);',
+      errors: [
+        {
+          column: 1,
+          endColumn: 7,
+          messageId: 'unnecessaryTypeConversion',
+          suggestions: [
+            {
+              messageId: 'suggestRemove',
+              output: '3n;',
+            },
+            {
+              messageId: 'suggestSatisfies',
+              output: '3n satisfies bigint;',
+            },
+          ],
+        },
+      ],
+    },
+
+    // using type conversion idioms on generics that extend primitives is invalid
+    {
+      code: `
+        function f<T extends string>(x: T) {
+          return String(x);
+        }
+      `,
+      errors: [
+        {
+          column: 18,
+          endColumn: 24,
+          endLine: 3,
+          line: 3,
+          messageId: 'unnecessaryTypeConversion',
+          suggestions: [
+            {
+              messageId: 'suggestRemove',
+              output: `
+        function f<T extends string>(x: T) {
+          return x;
+        }
+      `,
+            },
+            {
+              messageId: 'suggestSatisfies',
+              output: `
+        function f<T extends string>(x: T) {
+          return x satisfies string;
+        }
+      `,
+            },
+          ],
+        },
+      ],
+    },
+    {
+      code: `
+        function f<T extends number>(x: T) {
+          return Number(x);
+        }
+      `,
+      errors: [
+        {
+          column: 18,
+          endColumn: 24,
+          endLine: 3,
+          line: 3,
+          messageId: 'unnecessaryTypeConversion',
+          suggestions: [
+            {
+              messageId: 'suggestRemove',
+              output: `
+        function f<T extends number>(x: T) {
+          return x;
+        }
+      `,
+            },
+            {
+              messageId: 'suggestSatisfies',
+              output: `
+        function f<T extends number>(x: T) {
+          return x satisfies number;
+        }
+      `,
+            },
+          ],
+        },
+      ],
+    },
+    {
+      code: `
+        function f<T extends boolean>(x: T) {
+          return Boolean(x);
+        }
+      `,
+      errors: [
+        {
+          column: 18,
+          endColumn: 25,
+          endLine: 3,
+          line: 3,
+          messageId: 'unnecessaryTypeConversion',
+          suggestions: [
+            {
+              messageId: 'suggestRemove',
+              output: `
+        function f<T extends boolean>(x: T) {
+          return x;
+        }
+      `,
+            },
+            {
+              messageId: 'suggestSatisfies',
+              output: `
+        function f<T extends boolean>(x: T) {
+          return x satisfies boolean;
+        }
+      `,
+            },
+          ],
+        },
+      ],
+    },
+    {
+      code: `
+        function f<T extends bigint>(x: T) {
+          return BigInt(x);
+        }
+      `,
+      errors: [
+        {
+          column: 18,
+          endColumn: 24,
+          endLine: 3,
+          line: 3,
+          messageId: 'unnecessaryTypeConversion',
+          suggestions: [
+            {
+              messageId: 'suggestRemove',
+              output: `
+        function f<T extends bigint>(x: T) {
+          return x;
+        }
+      `,
+            },
+            {
+              messageId: 'suggestSatisfies',
+              output: `
+        function f<T extends bigint>(x: T) {
+          return x satisfies bigint;
+        }
+      `,
+            },
+          ],
+        },
+      ],
+    },
+
+    // make sure fixes preserve parentheses in cases where logic would otherwise break
+    {
+      code: "String('a' + 'b').length;",
+      errors: [
+        {
+          column: 1,
+          endColumn: 7,
+          messageId: 'unnecessaryTypeConversion',
+          suggestions: [
+            {
+              messageId: 'suggestRemove',
+              output: "('a' + 'b').length;",
+            },
+            {
+              messageId: 'suggestSatisfies',
+              output: "(('a' + 'b') satisfies string).length;",
+            },
+          ],
+        },
+      ],
+    },
+    {
+      code: "('a' + 'b').toString().length;",
+      errors: [
+        {
+          column: 13,
+          endColumn: 23,
+          messageId: 'unnecessaryTypeConversion',
+          suggestions: [
+            {
+              messageId: 'suggestRemove',
+              output: "('a' + 'b').length;",
+            },
+            {
+              messageId: 'suggestSatisfies',
+              output: "(('a' + 'b') satisfies string).length;",
+            },
+          ],
+        },
+      ],
+    },
+    {
+      code: '2 * +(2 + 2);',
+      errors: [
+        {
+          column: 5,
+          endColumn: 6,
+          messageId: 'unnecessaryTypeConversion',
+          suggestions: [
+            {
+              messageId: 'suggestRemove',
+              output: '2 * (2 + 2);',
+            },
+            {
+              messageId: 'suggestSatisfies',
+              output: '2 * ((2 + 2) satisfies number);',
+            },
+          ],
+        },
+      ],
+    },
+    {
+      code: '2 * Number(2 + 2);',
+      errors: [
+        {
+          column: 5,
+          endColumn: 11,
+          messageId: 'unnecessaryTypeConversion',
+          suggestions: [
+            {
+              messageId: 'suggestRemove',
+              output: '2 * (2 + 2);',
+            },
+            {
+              messageId: 'suggestSatisfies',
+              output: '2 * ((2 + 2) satisfies number);',
+            },
+          ],
+        },
+      ],
+    },
+    {
+      code: '2 * ~~(2 + 2);',
+      errors: [
+        {
+          column: 5,
+          endColumn: 7,
+          messageId: 'unnecessaryTypeConversion',
+          suggestions: [
+            {
+              messageId: 'suggestRemove',
+              output: '2 * (2 + 2);',
+            },
+            {
+              messageId: 'suggestSatisfies',
+              output: '2 * ((2 + 2) satisfies number);',
+            },
+          ],
+        },
+      ],
+    },
+    {
+      code: 'false && !!(false || true);',
+      errors: [
+        {
+          column: 10,
+          endColumn: 12,
+          messageId: 'unnecessaryTypeConversion',
+          suggestions: [
+            {
+              messageId: 'suggestRemove',
+              output: 'false && (false || true);',
+            },
+            {
+              messageId: 'suggestSatisfies',
+              output: 'false && ((false || true) satisfies boolean);',
+            },
+          ],
+        },
+      ],
+    },
+    {
+      code: 'false && Boolean(false || true);',
+      errors: [
+        {
+          column: 10,
+          endColumn: 17,
+          messageId: 'unnecessaryTypeConversion',
+          suggestions: [
+            {
+              messageId: 'suggestRemove',
+              output: 'false && (false || true);',
+            },
+            {
+              messageId: 'suggestSatisfies',
+              output: 'false && ((false || true) satisfies boolean);',
+            },
+          ],
+        },
+      ],
+    },
+    {
+      code: '2n * BigInt(2n + 2n);',
+      errors: [
+        {
+          column: 6,
+          endColumn: 12,
+          messageId: 'unnecessaryTypeConversion',
+          suggestions: [
+            {
+              messageId: 'suggestRemove',
+              output: '2n * (2n + 2n);',
+            },
+            {
+              messageId: 'suggestSatisfies',
+              output: '2n * ((2n + 2n) satisfies bigint);',
+            },
+          ],
+        },
+      ],
+    },
+
+    // make sure suggestions add parentheses in cases where syntax would otherwise break
+    {
+      code: `
+        let str = 'asdf';
+        String(str).length;
+      `,
+      errors: [
+        {
+          column: 9,
+          endColumn: 15,
+          endLine: 3,
+          line: 3,
+          messageId: 'unnecessaryTypeConversion',
+          suggestions: [
+            {
+              messageId: 'suggestRemove',
+              output: `
+        let str = 'asdf';
+        str.length;
+      `,
+            },
+            {
+              messageId: 'suggestSatisfies',
+              output: `
+        let str = 'asdf';
+        (str satisfies string).length;
+      `,
+            },
+          ],
+        },
+      ],
+    },
+    {
+      code: `
+        let str = 'asdf';
+        str.toString().length;
+      `,
+      errors: [
+        {
+          column: 13,
+          endColumn: 23,
+          messageId: 'unnecessaryTypeConversion',
+          suggestions: [
+            {
+              messageId: 'suggestRemove',
+              output: `
+        let str = 'asdf';
+        str.length;
+      `,
+            },
+            {
+              messageId: 'suggestSatisfies',
+              output: `
+        let str = 'asdf';
+        (str satisfies string).length;
+      `,
+            },
+          ],
+        },
+      ],
+    },
+  ],
+});
diff --git a/packages/eslint-plugin/tests/rules/promise-function-async.test.ts b/packages/eslint-plugin/tests/rules/promise-function-async.test.ts
index da1999e93e82..2b6ddbdb9870 100644
--- a/packages/eslint-plugin/tests/rules/promise-function-async.test.ts
+++ b/packages/eslint-plugin/tests/rules/promise-function-async.test.ts
@@ -4,7 +4,6 @@ import rule from '../../src/rules/promise-function-async';
 import { getFixturesRootDir } from '../RuleTester';
 
 const rootDir = getFixturesRootDir();
-const messageId = 'missingAsync';
 
 const ruleTester = new RuleTester({
   languageOptions: {
@@ -238,7 +237,7 @@ function returnsAny(): any {
       `,
       errors: [
         {
-          messageId,
+          messageId: 'missingAsync',
         },
       ],
       options: [
@@ -256,7 +255,7 @@ function returnsUnknown(): unknown {
       `,
       errors: [
         {
-          messageId,
+          messageId: 'missingAsync',
         },
       ],
       options: [
@@ -274,7 +273,7 @@ const nonAsyncPromiseFunctionExpressionA = function (p: Promise<void>) {
       `,
       errors: [
         {
-          messageId,
+          messageId: 'missingAsync',
         },
       ],
       output: `
@@ -291,7 +290,7 @@ const nonAsyncPromiseFunctionExpressionB = function () {
       `,
       errors: [
         {
-          messageId,
+          messageId: 'missingAsync',
         },
       ],
       output: `
@@ -308,7 +307,7 @@ function nonAsyncPromiseFunctionDeclarationA(p: Promise<void>) {
       `,
       errors: [
         {
-          messageId,
+          messageId: 'missingAsync',
         },
       ],
       output: `
@@ -325,7 +324,7 @@ function nonAsyncPromiseFunctionDeclarationB() {
       `,
       errors: [
         {
-          messageId,
+          messageId: 'missingAsync',
         },
       ],
       output: `
@@ -340,7 +339,7 @@ const nonAsyncPromiseArrowFunctionA = (p: Promise<void>) => p;
       `,
       errors: [
         {
-          messageId,
+          messageId: 'missingAsync',
         },
       ],
       output: `
@@ -353,7 +352,7 @@ const nonAsyncPromiseArrowFunctionB = () => new Promise<void>();
       `,
       errors: [
         {
-          messageId,
+          messageId: 'missingAsync',
         },
       ],
       output: `
@@ -371,7 +370,7 @@ const functions = {
       errors: [
         {
           line: 3,
-          messageId,
+          messageId: 'missingAsync',
         },
       ],
       output: `
@@ -397,11 +396,11 @@ class Test {
       errors: [
         {
           line: 3,
-          messageId,
+          messageId: 'missingAsync',
         },
         {
           line: 7,
-          messageId,
+          messageId: 'missingAsync',
         },
       ],
       output: `
@@ -437,15 +436,15 @@ class Test {
       errors: [
         {
           line: 2,
-          messageId,
+          messageId: 'missingAsync',
         },
         {
           line: 6,
-          messageId,
+          messageId: 'missingAsync',
         },
         {
           line: 13,
-          messageId,
+          messageId: 'missingAsync',
         },
       ],
       options: [
@@ -492,15 +491,15 @@ class Test {
       errors: [
         {
           line: 2,
-          messageId,
+          messageId: 'missingAsync',
         },
         {
           line: 10,
-          messageId,
+          messageId: 'missingAsync',
         },
         {
           line: 13,
-          messageId,
+          messageId: 'missingAsync',
         },
       ],
       options: [
@@ -547,15 +546,15 @@ class Test {
       errors: [
         {
           line: 6,
-          messageId,
+          messageId: 'missingAsync',
         },
         {
           line: 10,
-          messageId,
+          messageId: 'missingAsync',
         },
         {
           line: 13,
-          messageId,
+          messageId: 'missingAsync',
         },
       ],
       options: [
@@ -602,15 +601,15 @@ class Test {
       errors: [
         {
           line: 2,
-          messageId,
+          messageId: 'missingAsync',
         },
         {
           line: 6,
-          messageId,
+          messageId: 'missingAsync',
         },
         {
           line: 10,
-          messageId,
+          messageId: 'missingAsync',
         },
       ],
       options: [
@@ -645,7 +644,7 @@ const returnAllowedType = () => new PromiseType();
       errors: [
         {
           line: 4,
-          messageId,
+          messageId: 'missingAsync',
         },
       ],
       options: [
@@ -671,7 +670,7 @@ function foo(): Promise<string> | SPromise<boolean> {
       errors: [
         {
           line: 3,
-          messageId,
+          messageId: 'missingAsync',
         },
       ],
       options: [
@@ -697,7 +696,7 @@ class Test {
   }
 }
       `,
-      errors: [{ column: 3, line: 4, messageId }],
+      errors: [{ column: 3, line: 4, messageId: 'missingAsync' }],
       output: `
 class Test {
   @decorator
@@ -723,9 +722,9 @@ class Test {
 }
       `,
       errors: [
-        { column: 3, line: 4, messageId },
-        { column: 3, line: 7, messageId },
-        { column: 3, line: 10, messageId },
+        { column: 3, line: 4, messageId: 'missingAsync' },
+        { column: 3, line: 7, messageId: 'missingAsync' },
+        { column: 3, line: 10, messageId: 'missingAsync' },
       ],
       output: `
 class Test {
@@ -764,17 +763,17 @@ class Foo {
         {
           column: 3,
           line: 3,
-          messageId,
+          messageId: 'missingAsync',
         },
         {
           column: 3,
           line: 7,
-          messageId,
+          messageId: 'missingAsync',
         },
         {
           column: 3,
           line: 12,
-          messageId,
+          messageId: 'missingAsync',
         },
       ],
       output: `
@@ -806,7 +805,7 @@ const foo = {
         {
           column: 3,
           line: 3,
-          messageId,
+          messageId: 'missingAsync',
         },
       ],
       output: `
@@ -905,7 +904,7 @@ function overloadingThatCanReturnPromise(
       `,
       errors: [
         {
-          messageId,
+          messageId: 'missingAsync',
         },
       ],
       output: `
@@ -928,7 +927,7 @@ function overloadingThatIncludeAny(a?: boolean): any | number {
       `,
       errors: [
         {
-          messageId,
+          messageId: 'missingAsync',
         },
       ],
       options: [{ allowAny: false }],
@@ -943,7 +942,7 @@ function overloadingThatIncludeUnknown(a?: boolean): unknown | number {
       `,
       errors: [
         {
-          messageId,
+          messageId: 'missingAsync',
         },
       ],
       options: [{ allowAny: false }],
diff --git a/packages/eslint-plugin/tests/schema-snapshots/no-unnecessary-type-conversion.shot b/packages/eslint-plugin/tests/schema-snapshots/no-unnecessary-type-conversion.shot
new file mode 100644
index 000000000000..42f81875ed94
--- /dev/null
+++ b/packages/eslint-plugin/tests/schema-snapshots/no-unnecessary-type-conversion.shot
@@ -0,0 +1,10 @@
+
+# SCHEMA:
+
+[]
+
+
+# TYPES:
+
+/** No options declared */
+type Options = [];
\ No newline at end of file
diff --git a/packages/website/src/components/ast/HiddenItem.tsx b/packages/website/src/components/ast/HiddenItem.tsx
index 39aa997bfe54..dd3591fc28ed 100644
--- a/packages/website/src/components/ast/HiddenItem.tsx
+++ b/packages/website/src/components/ast/HiddenItem.tsx
@@ -42,7 +42,7 @@ export default function HiddenItem({
         value.map(([key], index) => (
           <span key={`${level}_[${index}]`}>
             {index > 0 && ', '}
-            {String(key)}
+            {key}
           </span>
         ))
       )}
diff --git a/packages/website/src/components/config/ConfigEditor.tsx b/packages/website/src/components/config/ConfigEditor.tsx
index 66411357347a..2ffc94af3294 100644
--- a/packages/website/src/components/config/ConfigEditor.tsx
+++ b/packages/website/src/components/config/ConfigEditor.tsx
@@ -35,7 +35,7 @@ function filterConfig(
   return options
     .map(group => ({
       fields: group.fields.filter(item =>
-        String(item.key.toLowerCase()).includes(filter.toLowerCase()),
+        item.key.toLowerCase().includes(filter.toLowerCase()),
       ),
       heading: group.heading,
     }))


Reviews:
user: login: kirkwaiblinger
body: 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.

🙂  
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2024-10-21T06:50:34Z
user: login: kirkwaiblinger
state: COMMENTED
author_association: MEMBER
submitted_at: 2024-10-21T06:59:00Z
user: login: kirkwaiblinger
state: COMMENTED
author_association: MEMBER
submitted_at: 2024-10-21T06:59:21Z
user: login: skondrashov
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-11-03T13:26:55Z
user: login: skondrashov
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-11-03T14:11:41Z
user: login: skondrashov
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-11-03T14:22:09Z
user: login: skondrashov
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-01-26T06:49:53Z
user: login: kirkwaiblinger
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-01-29T06:54:22Z
user: login: kirkwaiblinger
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-01-29T07:11:17Z
user: login: kirkwaiblinger
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-01-29T07:20:40Z
user: login: kirkwaiblinger
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-01-29T07:34:16Z
user: login: kirkwaiblinger
body: Responded to some comments for now, but don't have time to finish a thorough review at the moment, will come back to this. Once again thanks for your detailed work and discussions!
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-01-29T07:37:45Z
user: login: kirkwaiblinger
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-01-29T07:40:23Z
user: login: kirkwaiblinger
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-01-29T07:46:42Z
user: login: kirkwaiblinger
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-01-29T07:48:31Z
user: login: skondrashov
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-02T19:56:18Z
user: login: skondrashov
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-02T20:33:27Z
user: login: kirkwaiblinger
body: Another good iteration! Looking forward to the next round! 🙂 
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-02-10T03:43:42Z
user: login: kirkwaiblinger
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-10T08:12:21Z
user: login: skondrashov
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-23T23:10:39Z
user: login: skondrashov
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-24T00:14:34Z
user: login: skondrashov
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-05T02:35:50Z
user: login: skondrashov
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-05T03:15:27Z
user: login: kirkwaiblinger
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-01T15:03:42Z
user: login: kirkwaiblinger
body: I think I'm happy with this! Thanks for your very detailed work on this. Outstanding first PR!
state: DISMISSED
author_association: MEMBER
submitted_at: 2025-05-01T15:34:48Z
user: login: kirkwaiblinger
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-01T15:36:17Z
user: login: skondrashov
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-01T17:10:06Z
user: login: kirkwaiblinger
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-02T03:18:12Z
user: login: kirkwaiblinger
state: DISMISSED
author_association: MEMBER
submitted_at: 2025-05-02T03:20:16Z
user: login: skondrashov
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T12:10:50Z
user: login: JoshuaKGoldberg
body: 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.  
state: DISMISSED
author_association: MEMBER
submitted_at: 2025-05-02T18:55:22Z
user: login: kirkwaiblinger
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-05T04:20:14Z
user: login: JoshuaKGoldberg
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-05T15:09:05Z
user: login: JoshuaKGoldberg
body: ![Red wavy text with gradient yellow to reddish orange 3d effect: "WONDERFUL"](https://github.com/user-attachments/assets/85346d34-6795-4c3c-ba0f-23b20c7c8dbe)

state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-05T15:09:47Z

Review comments:
diff_hunk: @@ -0,0 +1,242 @@
+import type { TSESTree } from '@typescript-eslint/utils';
+import { AST_NODE_TYPES } from '@typescript-eslint/utils';
+import type { RuleFix } from '@typescript-eslint/utils/ts-eslint';
+import * as ts from 'typescript';
+
+import {
+  createRule,
+  getConstrainedTypeAtLocation,
+  getParserServices,
+  isTypeFlagSet,
+} from '../util';
+
+type Options = [];
+type MessageIds = 'unnecessaryCoercion';
+
+export default createRule<Options, MessageIds>({
+  name: 'no-unnecessary-coercion',
+  meta: {
+    docs: {
+      description:
+        'Disallow conversion idioms when they do not change the type or value of the expression',
+      requiresTypeChecking: true,
+    },
+    fixable: 'code',
+    messages: {
+      unnecessaryCoercion:
+        '{{violation}} does not change the type or value of the {{type}}.',
+    },
+    schema: [],
+    type: 'suggestion',
+  },
+  defaultOptions: [],
+  create(context) {
+    function doesUnderlyingTypeMatchFlag(
+      type: ts.Type,
+      typeFlag: ts.TypeFlags,
+    ): boolean {
+      const isString = (t: ts.Type): boolean => {
+        return isTypeFlagSet(t, typeFlag);
+      };
+
+      if (type.isUnion()) {
+        return type.types.every(isString);
+      }
+
+      if (type.isIntersection()) {
+        return type.types.some(isString);
+      }
+
+      return isString(type);
+    }
+
+    const services = getParserServices(context);
+
+    return {
+      CallExpression(node: TSESTree.CallExpression): void {
+        if (
+          node.callee.type === AST_NODE_TYPES.Identifier &&
+          node.arguments.length === 1
+        ) {
+          const type = getConstrainedTypeAtLocation(
+            services,
+            node.arguments[0],
+          );
+
+          if (
+            (doesUnderlyingTypeMatchFlag(type, ts.TypeFlags.StringLike) &&
+              // eslint-disable-next-line @typescript-eslint/internal/prefer-ast-types-enum
+              node.callee.name === 'String') ||
+            (doesUnderlyingTypeMatchFlag(type, ts.TypeFlags.NumberLike) &&
+              node.callee.name === 'Number') ||
+            (doesUnderlyingTypeMatchFlag(type, ts.TypeFlags.BooleanLike) &&
+              // eslint-disable-next-line @typescript-eslint/internal/prefer-ast-types-enum
+              node.callee.name === 'Boolean') ||
+            (doesUnderlyingTypeMatchFlag(type, ts.TypeFlags.BigIntLike) &&
+              node.callee.name === 'BigInt')
+          ) {
+            context.report({
+              node,
+              messageId: 'unnecessaryCoercion',
+              fix: (fixer): RuleFix[] => [
+                fixer.removeRange([node.range[0], node.arguments[0].range[0]]),
+                fixer.removeRange([node.arguments[0].range[1], node.range[1]]),
+              ],
+              data: {
+                violation: `Passing a ${node.callee.name.toLowerCase()} to ${node.callee.name}()`,
+                type: node.callee.name.toLowerCase(),
+              },
+            });
+          }
+        }
+      },
+      'CallExpression > MemberExpression.callee > Identifier[name = "toString"].property'(
+        node: TSESTree.Expression,
+      ): void {
+        const memberExpr = node.parent as TSESTree.MemberExpression;
+        const type = getConstrainedTypeAtLocation(services, memberExpr.object);
+        if (doesUnderlyingTypeMatchFlag(type, ts.TypeFlags.StringLike)) {
+          context.report({
+            node,
+            messageId: 'unnecessaryCoercion',
+            fix: (fixer): RuleFix[] => [
+              fixer.removeRange([
+                memberExpr.parent.range[0],
+                memberExpr.object.range[0],
+              ]),
+              fixer.removeRange([
+                memberExpr.object.range[1],
+                memberExpr.parent.range[1],
+              ]),
+            ],
+            loc: {
+              start: memberExpr.object.loc.end,
+              end: memberExpr.parent.loc.end,
+            },
+            data: {
+              violation: 'Using .toString() on a string',
+              type: 'string',
+            },
+          });
+        }
+      },
+      'AssignmentExpression[operator = "+="], BinaryExpression[operator = "+"]'(
+        node: TSESTree.AssignmentExpression | TSESTree.BinaryExpression,
+      ): void {
+        const leftType = services.getTypeAtLocation(node.left);
+        const rightType = services.getTypeAtLocation(node.right);
+        if (
+          doesUnderlyingTypeMatchFlag(leftType, ts.TypeFlags.StringLike) &&
+          node.right.type === AST_NODE_TYPES.Literal &&
+          node.right.value === ''
+        ) {
+          context.report({
+            node,
+            messageId: 'unnecessaryCoercion',
+            fix: (fixer): RuleFix[] => [
+              fixer.removeRange([node.range[0], node.left.range[0]]),
+              fixer.removeRange([node.left.range[1], node.range[1]]),
+            ],
+            loc: {
+              start: node.left.loc.end,
+              end: node.loc.end,
+            },
+            data: {
+              violation: "Concatenating a string with ''",
+              type: 'string',
+            },
+          });
+        }
+        if (
+          node.left.type === AST_NODE_TYPES.Literal &&
+          node.left.value === '' &&
+          doesUnderlyingTypeMatchFlag(rightType, ts.TypeFlags.StringLike)
+        ) {
+          context.report({
+            node,
+            messageId: 'unnecessaryCoercion',
+            fix: (fixer): RuleFix[] => [
+              fixer.removeRange([node.range[0], node.right.range[0]]),
+              fixer.removeRange([node.right.range[1], node.range[1]]),
+            ],
+            loc: {
+              start: node.loc.start,
+              end: node.right.loc.start,
+            },
+            data: {
+              violation: "Concatenating '' with a string",
+              type: 'string',
+            },
+          });
+        }
+      },
+      'UnaryExpression[operator = "+"]'(node: TSESTree.UnaryExpression): void {
path: packages/eslint-plugin/src/rules/no-unnecessary-coercion.ts
user: login: kirkwaiblinger
body: I think we should exempt numeric literals... i.e.
```ts
const changeInSomeQuantity = +100;
```

IMO this isn't an example of coercion, really, just an example of a verbose syntax to declare a positive number. People can easily use no-restricted-syntax to ban `+123` specifically if they want.

WDYT?
created_at: 2024-10-21T05:06:27Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,242 @@
+import type { TSESTree } from '@typescript-eslint/utils';
+import { AST_NODE_TYPES } from '@typescript-eslint/utils';
+import type { RuleFix } from '@typescript-eslint/utils/ts-eslint';
+import * as ts from 'typescript';
+
+import {
+  createRule,
+  getConstrainedTypeAtLocation,
+  getParserServices,
+  isTypeFlagSet,
+} from '../util';
+
+type Options = [];
+type MessageIds = 'unnecessaryCoercion';
+
+export default createRule<Options, MessageIds>({
+  name: 'no-unnecessary-coercion',
+  meta: {
+    docs: {
+      description:
+        'Disallow conversion idioms when they do not change the type or value of the expression',
+      requiresTypeChecking: true,
+    },
+    fixable: 'code',
+    messages: {
+      unnecessaryCoercion:
+        '{{violation}} does not change the type or value of the {{type}}.',
+    },
+    schema: [],
+    type: 'suggestion',
+  },
+  defaultOptions: [],
+  create(context) {
+    function doesUnderlyingTypeMatchFlag(
+      type: ts.Type,
+      typeFlag: ts.TypeFlags,
+    ): boolean {
+      const isString = (t: ts.Type): boolean => {
+        return isTypeFlagSet(t, typeFlag);
+      };
+
+      if (type.isUnion()) {
+        return type.types.every(isString);
+      }
+
+      if (type.isIntersection()) {
+        return type.types.some(isString);
+      }
+
+      return isString(type);
+    }
+
+    const services = getParserServices(context);
+
+    return {
+      CallExpression(node: TSESTree.CallExpression): void {
+        if (
+          node.callee.type === AST_NODE_TYPES.Identifier &&
+          node.arguments.length === 1
+        ) {
+          const type = getConstrainedTypeAtLocation(
+            services,
+            node.arguments[0],
+          );
+
+          if (
+            (doesUnderlyingTypeMatchFlag(type, ts.TypeFlags.StringLike) &&
+              // eslint-disable-next-line @typescript-eslint/internal/prefer-ast-types-enum
+              node.callee.name === 'String') ||
+            (doesUnderlyingTypeMatchFlag(type, ts.TypeFlags.NumberLike) &&
+              node.callee.name === 'Number') ||
+            (doesUnderlyingTypeMatchFlag(type, ts.TypeFlags.BooleanLike) &&
+              // eslint-disable-next-line @typescript-eslint/internal/prefer-ast-types-enum
+              node.callee.name === 'Boolean') ||
+            (doesUnderlyingTypeMatchFlag(type, ts.TypeFlags.BigIntLike) &&
+              node.callee.name === 'BigInt')
+          ) {
+            context.report({
path: packages/eslint-plugin/src/rules/no-unnecessary-coercion.ts
user: login: kirkwaiblinger
body: Let's be sure to use utilities for the fixers so that we consider precedence in the fix. For instance, right now, this
```ts
console.log(false && !!(false || true)) // logs false
```
fixes to 
```ts
console.log(false && false || true) // logs true
```

Just look to other rules for examples... I think no-unnecessary-template-expression has very similar fixer logic.
created_at: 2024-10-21T05:37:39Z
author_association: MEMBER
path: packages/eslint-plugin/src/rules/no-unnecessary-coercion.ts
user: login: kirkwaiblinger
body: My opinion is that we should
1. Change all these fixes to suggestions
2. Provide a second suggestion to use the `satisfies` operator. i.e. `!!booleanVariable` -> `booleanVariable satisfies boolean`
created_at: 2024-10-21T05:40:38Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,242 @@
+import type { TSESTree } from '@typescript-eslint/utils';
+import { AST_NODE_TYPES } from '@typescript-eslint/utils';
+import type { RuleFix } from '@typescript-eslint/utils/ts-eslint';
+import * as ts from 'typescript';
+
+import {
+  createRule,
+  getConstrainedTypeAtLocation,
+  getParserServices,
+  isTypeFlagSet,
+} from '../util';
+
+type Options = [];
+type MessageIds = 'unnecessaryCoercion';
+
+export default createRule<Options, MessageIds>({
+  name: 'no-unnecessary-coercion',
+  meta: {
+    docs: {
+      description:
+        'Disallow conversion idioms when they do not change the type or value of the expression',
+      requiresTypeChecking: true,
+    },
+    fixable: 'code',
+    messages: {
+      unnecessaryCoercion:
+        '{{violation}} does not change the type or value of the {{type}}.',
+    },
+    schema: [],
+    type: 'suggestion',
+  },
+  defaultOptions: [],
+  create(context) {
+    function doesUnderlyingTypeMatchFlag(
+      type: ts.Type,
+      typeFlag: ts.TypeFlags,
+    ): boolean {
+      const isString = (t: ts.Type): boolean => {
+        return isTypeFlagSet(t, typeFlag);
+      };
+
+      if (type.isUnion()) {
+        return type.types.every(isString);
+      }
+
+      if (type.isIntersection()) {
+        return type.types.some(isString);
+      }
+
+      return isString(type);
+    }
+
+    const services = getParserServices(context);
+
+    return {
+      CallExpression(node: TSESTree.CallExpression): void {
path: packages/eslint-plugin/src/rules/no-unnecessary-coercion.ts
user: login: kirkwaiblinger
body: Here and elsewhere, it's generally a good practice to only access type information if it's needed, since type information is a) expensive and b) computed lazily (and cached) by TypeScript.

For instance, in this handler, you only need to get the type information if the `node.callee.name` is actually 'String', 'Number', etc., which is a cheap syntactic check, rather than on every single CallExpression with an identifier as the callee and a single argument in the argument list.
created_at: 2024-10-21T05:46:21Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,242 @@
+import type { TSESTree } from '@typescript-eslint/utils';
+import { AST_NODE_TYPES } from '@typescript-eslint/utils';
+import type { RuleFix } from '@typescript-eslint/utils/ts-eslint';
+import * as ts from 'typescript';
+
+import {
+  createRule,
+  getConstrainedTypeAtLocation,
+  getParserServices,
+  isTypeFlagSet,
+} from '../util';
+
+type Options = [];
+type MessageIds = 'unnecessaryCoercion';
+
+export default createRule<Options, MessageIds>({
+  name: 'no-unnecessary-coercion',
+  meta: {
+    docs: {
+      description:
+        'Disallow conversion idioms when they do not change the type or value of the expression',
+      requiresTypeChecking: true,
+    },
+    fixable: 'code',
+    messages: {
+      unnecessaryCoercion:
+        '{{violation}} does not change the type or value of the {{type}}.',
+    },
+    schema: [],
+    type: 'suggestion',
+  },
+  defaultOptions: [],
+  create(context) {
+    function doesUnderlyingTypeMatchFlag(
+      type: ts.Type,
+      typeFlag: ts.TypeFlags,
+    ): boolean {
+      const isString = (t: ts.Type): boolean => {
path: packages/eslint-plugin/src/rules/no-unnecessary-coercion.ts
user: login: kirkwaiblinger
body: (nit) name has diverged from implementation
Maybe
```suggestion
      const matchesType = (t: ts.Type): boolean => {
```
or something else?  🤷 
created_at: 2024-10-21T05:48:35Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,242 @@
+import type { TSESTree } from '@typescript-eslint/utils';
+import { AST_NODE_TYPES } from '@typescript-eslint/utils';
+import type { RuleFix } from '@typescript-eslint/utils/ts-eslint';
+import * as ts from 'typescript';
+
+import {
+  createRule,
+  getConstrainedTypeAtLocation,
+  getParserServices,
+  isTypeFlagSet,
+} from '../util';
+
+type Options = [];
+type MessageIds = 'unnecessaryCoercion';
+
+export default createRule<Options, MessageIds>({
+  name: 'no-unnecessary-coercion',
+  meta: {
+    docs: {
+      description:
+        'Disallow conversion idioms when they do not change the type or value of the expression',
+      requiresTypeChecking: true,
+    },
+    fixable: 'code',
+    messages: {
+      unnecessaryCoercion:
+        '{{violation}} does not change the type or value of the {{type}}.',
+    },
+    schema: [],
+    type: 'suggestion',
+  },
+  defaultOptions: [],
+  create(context) {
+    function doesUnderlyingTypeMatchFlag(
+      type: ts.Type,
+      typeFlag: ts.TypeFlags,
+    ): boolean {
+      const isString = (t: ts.Type): boolean => {
+        return isTypeFlagSet(t, typeFlag);
+      };
+
+      if (type.isUnion()) {
+        return type.types.every(isString);
+      }
+
+      if (type.isIntersection()) {
+        return type.types.some(isString);
+      }
+
+      return isString(type);
+    }
+
+    const services = getParserServices(context);
+
+    return {
+      CallExpression(node: TSESTree.CallExpression): void {
+        if (
+          node.callee.type === AST_NODE_TYPES.Identifier &&
+          node.arguments.length === 1
+        ) {
+          const type = getConstrainedTypeAtLocation(
+            services,
+            node.arguments[0],
+          );
+
+          if (
path: packages/eslint-plugin/src/rules/no-unnecessary-coercion.ts
user: login: kirkwaiblinger
body: we are pedantic about using scope analysis to make sure that there isn't a local function named `String`/`Boolean`, etc, see, for example, https://github.com/typescript-eslint/typescript-eslint/pull/10005#discussion_r1771754649.
created_at: 2024-10-21T05:53:23Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,242 @@
+import type { TSESTree } from '@typescript-eslint/utils';
+import { AST_NODE_TYPES } from '@typescript-eslint/utils';
+import type { RuleFix } from '@typescript-eslint/utils/ts-eslint';
+import * as ts from 'typescript';
+
+import {
+  createRule,
+  getConstrainedTypeAtLocation,
+  getParserServices,
+  isTypeFlagSet,
+} from '../util';
+
+type Options = [];
+type MessageIds = 'unnecessaryCoercion';
+
+export default createRule<Options, MessageIds>({
+  name: 'no-unnecessary-coercion',
+  meta: {
+    docs: {
+      description:
+        'Disallow conversion idioms when they do not change the type or value of the expression',
+      requiresTypeChecking: true,
+    },
+    fixable: 'code',
+    messages: {
+      unnecessaryCoercion:
+        '{{violation}} does not change the type or value of the {{type}}.',
+    },
+    schema: [],
+    type: 'suggestion',
+  },
+  defaultOptions: [],
+  create(context) {
+    function doesUnderlyingTypeMatchFlag(
+      type: ts.Type,
+      typeFlag: ts.TypeFlags,
+    ): boolean {
+      const isString = (t: ts.Type): boolean => {
+        return isTypeFlagSet(t, typeFlag);
+      };
+
+      if (type.isUnion()) {
+        return type.types.every(isString);
+      }
+
+      if (type.isIntersection()) {
+        return type.types.some(isString);
+      }
+
+      return isString(type);
+    }
+
+    const services = getParserServices(context);
+
+    return {
+      CallExpression(node: TSESTree.CallExpression): void {
+        if (
+          node.callee.type === AST_NODE_TYPES.Identifier &&
+          node.arguments.length === 1
+        ) {
+          const type = getConstrainedTypeAtLocation(
+            services,
+            node.arguments[0],
+          );
+
+          if (
+            (doesUnderlyingTypeMatchFlag(type, ts.TypeFlags.StringLike) &&
+              // eslint-disable-next-line @typescript-eslint/internal/prefer-ast-types-enum
+              node.callee.name === 'String') ||
+            (doesUnderlyingTypeMatchFlag(type, ts.TypeFlags.NumberLike) &&
+              node.callee.name === 'Number') ||
+            (doesUnderlyingTypeMatchFlag(type, ts.TypeFlags.BooleanLike) &&
+              // eslint-disable-next-line @typescript-eslint/internal/prefer-ast-types-enum
+              node.callee.name === 'Boolean') ||
+            (doesUnderlyingTypeMatchFlag(type, ts.TypeFlags.BigIntLike) &&
+              node.callee.name === 'BigInt')
+          ) {
+            context.report({
+              node,
+              messageId: 'unnecessaryCoercion',
+              fix: (fixer): RuleFix[] => [
+                fixer.removeRange([node.range[0], node.arguments[0].range[0]]),
+                fixer.removeRange([node.arguments[0].range[1], node.range[1]]),
+              ],
+              data: {
+                violation: `Passing a ${node.callee.name.toLowerCase()} to ${node.callee.name}()`,
+                type: node.callee.name.toLowerCase(),
+              },
+            });
+          }
+        }
+      },
+      'CallExpression > MemberExpression.callee > Identifier[name = "toString"].property'(
+        node: TSESTree.Expression,
+      ): void {
+        const memberExpr = node.parent as TSESTree.MemberExpression;
+        const type = getConstrainedTypeAtLocation(services, memberExpr.object);
+        if (doesUnderlyingTypeMatchFlag(type, ts.TypeFlags.StringLike)) {
+          context.report({
+            node,
path: packages/eslint-plugin/src/rules/no-unnecessary-coercion.ts
user: login: kirkwaiblinger
body: (nit) Here and elsewhere, `node` and `loc` are redundant with each other, with `loc` taking priority if present. Let's omit `node` when it's not being used.
created_at: 2024-10-21T06:29:59Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,242 @@
+import type { TSESTree } from '@typescript-eslint/utils';
+import { AST_NODE_TYPES } from '@typescript-eslint/utils';
+import type { RuleFix } from '@typescript-eslint/utils/ts-eslint';
+import * as ts from 'typescript';
+
+import {
+  createRule,
+  getConstrainedTypeAtLocation,
+  getParserServices,
+  isTypeFlagSet,
+} from '../util';
+
+type Options = [];
+type MessageIds = 'unnecessaryCoercion';
+
+export default createRule<Options, MessageIds>({
+  name: 'no-unnecessary-coercion',
+  meta: {
+    docs: {
+      description:
+        'Disallow conversion idioms when they do not change the type or value of the expression',
+      requiresTypeChecking: true,
+    },
+    fixable: 'code',
+    messages: {
+      unnecessaryCoercion:
+        '{{violation}} does not change the type or value of the {{type}}.',
+    },
+    schema: [],
+    type: 'suggestion',
+  },
+  defaultOptions: [],
+  create(context) {
+    function doesUnderlyingTypeMatchFlag(
+      type: ts.Type,
+      typeFlag: ts.TypeFlags,
+    ): boolean {
+      const isString = (t: ts.Type): boolean => {
+        return isTypeFlagSet(t, typeFlag);
+      };
+
+      if (type.isUnion()) {
+        return type.types.every(isString);
+      }
+
+      if (type.isIntersection()) {
+        return type.types.some(isString);
+      }
+
+      return isString(type);
+    }
+
+    const services = getParserServices(context);
+
+    return {
+      CallExpression(node: TSESTree.CallExpression): void {
+        if (
+          node.callee.type === AST_NODE_TYPES.Identifier &&
+          node.arguments.length === 1
+        ) {
+          const type = getConstrainedTypeAtLocation(
+            services,
+            node.arguments[0],
+          );
+
+          if (
+            (doesUnderlyingTypeMatchFlag(type, ts.TypeFlags.StringLike) &&
+              // eslint-disable-next-line @typescript-eslint/internal/prefer-ast-types-enum
+              node.callee.name === 'String') ||
+            (doesUnderlyingTypeMatchFlag(type, ts.TypeFlags.NumberLike) &&
+              node.callee.name === 'Number') ||
+            (doesUnderlyingTypeMatchFlag(type, ts.TypeFlags.BooleanLike) &&
+              // eslint-disable-next-line @typescript-eslint/internal/prefer-ast-types-enum
+              node.callee.name === 'Boolean') ||
+            (doesUnderlyingTypeMatchFlag(type, ts.TypeFlags.BigIntLike) &&
+              node.callee.name === 'BigInt')
+          ) {
+            context.report({
+              node,
+              messageId: 'unnecessaryCoercion',
+              fix: (fixer): RuleFix[] => [
+                fixer.removeRange([node.range[0], node.arguments[0].range[0]]),
+                fixer.removeRange([node.arguments[0].range[1], node.range[1]]),
+              ],
+              data: {
+                violation: `Passing a ${node.callee.name.toLowerCase()} to ${node.callee.name}()`,
+                type: node.callee.name.toLowerCase(),
+              },
+            });
+          }
+        }
+      },
+      'CallExpression > MemberExpression.callee > Identifier[name = "toString"].property'(
+        node: TSESTree.Expression,
+      ): void {
+        const memberExpr = node.parent as TSESTree.MemberExpression;
+        const type = getConstrainedTypeAtLocation(services, memberExpr.object);
+        if (doesUnderlyingTypeMatchFlag(type, ts.TypeFlags.StringLike)) {
+          context.report({
+            node,
+            messageId: 'unnecessaryCoercion',
+            fix: (fixer): RuleFix[] => [
+              fixer.removeRange([
+                memberExpr.parent.range[0],
+                memberExpr.object.range[0],
+              ]),
+              fixer.removeRange([
+                memberExpr.object.range[1],
+                memberExpr.parent.range[1],
+              ]),
+            ],
+            loc: {
+              start: memberExpr.object.loc.end,
+              end: memberExpr.parent.loc.end,
+            },
+            data: {
+              violation: 'Using .toString() on a string',
+              type: 'string',
+            },
+          });
+        }
+      },
+      'AssignmentExpression[operator = "+="], BinaryExpression[operator = "+"]'(
+        node: TSESTree.AssignmentExpression | TSESTree.BinaryExpression,
+      ): void {
+        const leftType = services.getTypeAtLocation(node.left);
+        const rightType = services.getTypeAtLocation(node.right);
+        if (
+          doesUnderlyingTypeMatchFlag(leftType, ts.TypeFlags.StringLike) &&
+          node.right.type === AST_NODE_TYPES.Literal &&
+          node.right.value === ''
+        ) {
+          context.report({
+            node,
+            messageId: 'unnecessaryCoercion',
+            fix: (fixer): RuleFix[] => [
+              fixer.removeRange([node.range[0], node.left.range[0]]),
+              fixer.removeRange([node.left.range[1], node.range[1]]),
+            ],
+            loc: {
+              start: node.left.loc.end,
+              end: node.loc.end,
+            },
+            data: {
+              violation: "Concatenating a string with ''",
+              type: 'string',
+            },
+          });
+        }
+        if (
+          node.left.type === AST_NODE_TYPES.Literal &&
+          node.left.value === '' &&
+          doesUnderlyingTypeMatchFlag(rightType, ts.TypeFlags.StringLike)
+        ) {
+          context.report({
+            node,
+            messageId: 'unnecessaryCoercion',
+            fix: (fixer): RuleFix[] => [
+              fixer.removeRange([node.range[0], node.right.range[0]]),
+              fixer.removeRange([node.right.range[1], node.range[1]]),
+            ],
+            loc: {
+              start: node.loc.start,
+              end: node.right.loc.start,
+            },
+            data: {
+              violation: "Concatenating '' with a string",
+              type: 'string',
+            },
+          });
+        }
+      },
+      'UnaryExpression[operator = "+"]'(node: TSESTree.UnaryExpression): void {
+        const type = services.getTypeAtLocation(node.argument);
+        if (doesUnderlyingTypeMatchFlag(type, ts.TypeFlags.NumberLike)) {
+          context.report({
+            node,
+            messageId: 'unnecessaryCoercion',
+            fix: (fixer): RuleFix[] => [
+              fixer.removeRange([node.range[0], node.argument.range[0]]),
+              fixer.removeRange([node.argument.range[1], node.range[1]]),
+            ],
+            loc: {
+              start: node.loc.start,
+              end: node.argument.loc.start,
path: packages/eslint-plugin/src/rules/no-unnecessary-coercion.ts
user: login: kirkwaiblinger
body: For tokens of fixed size, you might try this approach, in order to avoid awkward situations with parentheses.

```suggestion
              end: node.loc.start + 1
```

A test case for this would be to ensure you get
```ts
!(num1 + num2)
~
```
rather than 
```ts
!(num1 + num2)
~~
```
created_at: 2024-10-21T06:32:38Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,212 @@
+import { RuleTester } from '@typescript-eslint/rule-tester';
+import { AST_NODE_TYPES } from '@typescript-eslint/utils';
+
+import rule from '../../src/rules/no-unnecessary-coercion';
+import { getFixturesRootDir } from '../RuleTester';
+
+const rootDir = getFixturesRootDir();
+
+const ruleTester = new RuleTester({
+  languageOptions: {
+    parserOptions: {
+      tsconfigRootDir: rootDir,
+      project: './tsconfig.json',
+    },
+  },
+});
+
+const messageId = 'unnecessaryCoercion';
+
+ruleTester.run('no-unnecessary-coercion', rule, {
+  valid: [
+    `
+String(1);
+    `,
+    `
+(1).toString();
+    `,
+    `
+\`\${1}\`;
+    `,
+    `
+'' + 1;
+    `,
+    `
+1 + '';
+    `,
+    `
+let str = 1;
+str += '';
+    `,
+    `
+Number('2');
+    `,
+    `
++'2';
+    `,
+    `
+~~'2';
+    `,
+    `
+Boolean(0);
+    `,
+    `
+!!0;
+    `,
+    `
+BigInt(3);
+    `,
+    `
+new String('asdf');
+    `,
+    `
+!false;
+    `,
+    `
+~256;
+    `,
+  ],
+
+  invalid: [
path: packages/eslint-plugin/tests/rules/no-unnecessary-coercion.test.ts
user: login: kirkwaiblinger
body: Couple general nits/info:
1. No need for the `type` in the `error` report. That asserts that the `node` provided in the `context.report()` is the provided type. However, that's useless here, since `node`'s only effect is to provide a report location if `loc` is not provided. And `loc` _is_ provided. So we're just not testing anything useful by that 😆 . 
2. Since you're using custom `loc` for the report locations, it's best to specify the full `line`, `endLine`, `column`, `endColumn` for the error. We're less careful about that when we just do the `node`'s location but here it makes sense to be more granular.
created_at: 2024-10-21T06:59:00Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,212 @@
+import { RuleTester } from '@typescript-eslint/rule-tester';
+import { AST_NODE_TYPES } from '@typescript-eslint/utils';
+
+import rule from '../../src/rules/no-unnecessary-coercion';
+import { getFixturesRootDir } from '../RuleTester';
+
+const rootDir = getFixturesRootDir();
+
+const ruleTester = new RuleTester({
+  languageOptions: {
+    parserOptions: {
+      tsconfigRootDir: rootDir,
+      project: './tsconfig.json',
+    },
+  },
+});
+
+const messageId = 'unnecessaryCoercion';
path: packages/eslint-plugin/tests/rules/no-unnecessary-coercion.test.ts
user: login: kirkwaiblinger
body: (nit) Eh, i'd inline this
created_at: 2024-10-21T06:59:21Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,242 @@
+import type { TSESTree } from '@typescript-eslint/utils';
+import { AST_NODE_TYPES } from '@typescript-eslint/utils';
+import type { RuleFix } from '@typescript-eslint/utils/ts-eslint';
+import * as ts from 'typescript';
+
+import {
+  createRule,
+  getConstrainedTypeAtLocation,
+  getParserServices,
+  isTypeFlagSet,
+} from '../util';
+
+type Options = [];
+type MessageIds = 'unnecessaryCoercion';
+
+export default createRule<Options, MessageIds>({
+  name: 'no-unnecessary-coercion',
+  meta: {
+    docs: {
+      description:
+        'Disallow conversion idioms when they do not change the type or value of the expression',
+      requiresTypeChecking: true,
+    },
+    fixable: 'code',
+    messages: {
+      unnecessaryCoercion:
+        '{{violation}} does not change the type or value of the {{type}}.',
+    },
+    schema: [],
+    type: 'suggestion',
+  },
+  defaultOptions: [],
+  create(context) {
+    function doesUnderlyingTypeMatchFlag(
+      type: ts.Type,
+      typeFlag: ts.TypeFlags,
+    ): boolean {
+      const isString = (t: ts.Type): boolean => {
+        return isTypeFlagSet(t, typeFlag);
+      };
+
+      if (type.isUnion()) {
+        return type.types.every(isString);
+      }
+
+      if (type.isIntersection()) {
+        return type.types.some(isString);
+      }
+
+      return isString(type);
+    }
+
+    const services = getParserServices(context);
+
+    return {
+      CallExpression(node: TSESTree.CallExpression): void {
+        if (
+          node.callee.type === AST_NODE_TYPES.Identifier &&
+          node.arguments.length === 1
+        ) {
+          const type = getConstrainedTypeAtLocation(
+            services,
+            node.arguments[0],
+          );
+
+          if (
+            (doesUnderlyingTypeMatchFlag(type, ts.TypeFlags.StringLike) &&
+              // eslint-disable-next-line @typescript-eslint/internal/prefer-ast-types-enum
+              node.callee.name === 'String') ||
+            (doesUnderlyingTypeMatchFlag(type, ts.TypeFlags.NumberLike) &&
+              node.callee.name === 'Number') ||
+            (doesUnderlyingTypeMatchFlag(type, ts.TypeFlags.BooleanLike) &&
+              // eslint-disable-next-line @typescript-eslint/internal/prefer-ast-types-enum
+              node.callee.name === 'Boolean') ||
+            (doesUnderlyingTypeMatchFlag(type, ts.TypeFlags.BigIntLike) &&
+              node.callee.name === 'BigInt')
+          ) {
+            context.report({
+              node,
+              messageId: 'unnecessaryCoercion',
+              fix: (fixer): RuleFix[] => [
+                fixer.removeRange([node.range[0], node.arguments[0].range[0]]),
+                fixer.removeRange([node.arguments[0].range[1], node.range[1]]),
+              ],
+              data: {
+                violation: `Passing a ${node.callee.name.toLowerCase()} to ${node.callee.name}()`,
+                type: node.callee.name.toLowerCase(),
+              },
+            });
+          }
+        }
+      },
+      'CallExpression > MemberExpression.callee > Identifier[name = "toString"].property'(
+        node: TSESTree.Expression,
+      ): void {
+        const memberExpr = node.parent as TSESTree.MemberExpression;
+        const type = getConstrainedTypeAtLocation(services, memberExpr.object);
+        if (doesUnderlyingTypeMatchFlag(type, ts.TypeFlags.StringLike)) {
+          context.report({
+            node,
+            messageId: 'unnecessaryCoercion',
+            fix: (fixer): RuleFix[] => [
+              fixer.removeRange([
+                memberExpr.parent.range[0],
+                memberExpr.object.range[0],
+              ]),
+              fixer.removeRange([
+                memberExpr.object.range[1],
+                memberExpr.parent.range[1],
+              ]),
+            ],
+            loc: {
+              start: memberExpr.object.loc.end,
+              end: memberExpr.parent.loc.end,
+            },
+            data: {
+              violation: 'Using .toString() on a string',
+              type: 'string',
+            },
+          });
+        }
+      },
+      'AssignmentExpression[operator = "+="], BinaryExpression[operator = "+"]'(
+        node: TSESTree.AssignmentExpression | TSESTree.BinaryExpression,
+      ): void {
+        const leftType = services.getTypeAtLocation(node.left);
+        const rightType = services.getTypeAtLocation(node.right);
+        if (
+          doesUnderlyingTypeMatchFlag(leftType, ts.TypeFlags.StringLike) &&
+          node.right.type === AST_NODE_TYPES.Literal &&
+          node.right.value === ''
+        ) {
+          context.report({
+            node,
+            messageId: 'unnecessaryCoercion',
+            fix: (fixer): RuleFix[] => [
+              fixer.removeRange([node.range[0], node.left.range[0]]),
+              fixer.removeRange([node.left.range[1], node.range[1]]),
+            ],
+            loc: {
+              start: node.left.loc.end,
+              end: node.loc.end,
+            },
+            data: {
+              violation: "Concatenating a string with ''",
+              type: 'string',
+            },
+          });
+        }
+        if (
+          node.left.type === AST_NODE_TYPES.Literal &&
+          node.left.value === '' &&
+          doesUnderlyingTypeMatchFlag(rightType, ts.TypeFlags.StringLike)
+        ) {
+          context.report({
+            node,
+            messageId: 'unnecessaryCoercion',
+            fix: (fixer): RuleFix[] => [
+              fixer.removeRange([node.range[0], node.right.range[0]]),
+              fixer.removeRange([node.right.range[1], node.range[1]]),
+            ],
+            loc: {
+              start: node.loc.start,
+              end: node.right.loc.start,
+            },
+            data: {
+              violation: "Concatenating '' with a string",
+              type: 'string',
+            },
+          });
+        }
+      },
+      'UnaryExpression[operator = "+"]'(node: TSESTree.UnaryExpression): void {
path: packages/eslint-plugin/src/rules/no-unnecessary-coercion.ts
user: login: skondrashov
body: I see what you mean. Not really sure. The error message I landed on makes me feel like I'm ok with still flagging these:

```
Using the unary + operator on a number does not change the type or value of the number
```

I'd assume the reason people would want to verbosely declare positive numbers would commonly have to do with having positive and negative counterparts for a number, eg `+100` and `-100`. Imagine then:

```
const PI = 3.14159
const positive_PI = +PI
const negative_PI = -PI
```

Now they're not using a number literal, but your reasoning still applies, they're just being verbose. Feels like a bit of a slippery slope? I would personally be using my rule (in line with what I generally expect from a linter) to tell me if I'm writing symbols that don't do anything. But it could be hard for a user in that context to see what that has to do with type conversion.
created_at: 2024-11-03T13:26:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,242 @@
+import type { TSESTree } from '@typescript-eslint/utils';
+import { AST_NODE_TYPES } from '@typescript-eslint/utils';
+import type { RuleFix } from '@typescript-eslint/utils/ts-eslint';
+import * as ts from 'typescript';
+
+import {
+  createRule,
+  getConstrainedTypeAtLocation,
+  getParserServices,
+  isTypeFlagSet,
+} from '../util';
+
+type Options = [];
+type MessageIds = 'unnecessaryCoercion';
+
+export default createRule<Options, MessageIds>({
+  name: 'no-unnecessary-coercion',
+  meta: {
+    docs: {
+      description:
+        'Disallow conversion idioms when they do not change the type or value of the expression',
+      requiresTypeChecking: true,
+    },
+    fixable: 'code',
+    messages: {
+      unnecessaryCoercion:
+        '{{violation}} does not change the type or value of the {{type}}.',
+    },
+    schema: [],
+    type: 'suggestion',
+  },
+  defaultOptions: [],
+  create(context) {
+    function doesUnderlyingTypeMatchFlag(
+      type: ts.Type,
+      typeFlag: ts.TypeFlags,
+    ): boolean {
+      const isString = (t: ts.Type): boolean => {
+        return isTypeFlagSet(t, typeFlag);
+      };
+
+      if (type.isUnion()) {
+        return type.types.every(isString);
+      }
+
+      if (type.isIntersection()) {
+        return type.types.some(isString);
+      }
+
+      return isString(type);
+    }
+
+    const services = getParserServices(context);
+
+    return {
+      CallExpression(node: TSESTree.CallExpression): void {
+        if (
+          node.callee.type === AST_NODE_TYPES.Identifier &&
+          node.arguments.length === 1
+        ) {
+          const type = getConstrainedTypeAtLocation(
+            services,
+            node.arguments[0],
+          );
+
+          if (
+            (doesUnderlyingTypeMatchFlag(type, ts.TypeFlags.StringLike) &&
+              // eslint-disable-next-line @typescript-eslint/internal/prefer-ast-types-enum
+              node.callee.name === 'String') ||
+            (doesUnderlyingTypeMatchFlag(type, ts.TypeFlags.NumberLike) &&
+              node.callee.name === 'Number') ||
+            (doesUnderlyingTypeMatchFlag(type, ts.TypeFlags.BooleanLike) &&
+              // eslint-disable-next-line @typescript-eslint/internal/prefer-ast-types-enum
+              node.callee.name === 'Boolean') ||
+            (doesUnderlyingTypeMatchFlag(type, ts.TypeFlags.BigIntLike) &&
+              node.callee.name === 'BigInt')
+          ) {
+            context.report({
+              node,
+              messageId: 'unnecessaryCoercion',
+              fix: (fixer): RuleFix[] => [
+                fixer.removeRange([node.range[0], node.arguments[0].range[0]]),
+                fixer.removeRange([node.arguments[0].range[1], node.range[1]]),
+              ],
+              data: {
+                violation: `Passing a ${node.callee.name.toLowerCase()} to ${node.callee.name}()`,
+                type: node.callee.name.toLowerCase(),
+              },
+            });
+          }
+        }
+      },
+      'CallExpression > MemberExpression.callee > Identifier[name = "toString"].property'(
+        node: TSESTree.Expression,
+      ): void {
+        const memberExpr = node.parent as TSESTree.MemberExpression;
+        const type = getConstrainedTypeAtLocation(services, memberExpr.object);
+        if (doesUnderlyingTypeMatchFlag(type, ts.TypeFlags.StringLike)) {
+          context.report({
+            node,
+            messageId: 'unnecessaryCoercion',
+            fix: (fixer): RuleFix[] => [
+              fixer.removeRange([
+                memberExpr.parent.range[0],
+                memberExpr.object.range[0],
+              ]),
+              fixer.removeRange([
+                memberExpr.object.range[1],
+                memberExpr.parent.range[1],
+              ]),
+            ],
+            loc: {
+              start: memberExpr.object.loc.end,
+              end: memberExpr.parent.loc.end,
+            },
+            data: {
+              violation: 'Using .toString() on a string',
+              type: 'string',
+            },
+          });
+        }
+      },
+      'AssignmentExpression[operator = "+="], BinaryExpression[operator = "+"]'(
+        node: TSESTree.AssignmentExpression | TSESTree.BinaryExpression,
+      ): void {
+        const leftType = services.getTypeAtLocation(node.left);
+        const rightType = services.getTypeAtLocation(node.right);
+        if (
+          doesUnderlyingTypeMatchFlag(leftType, ts.TypeFlags.StringLike) &&
+          node.right.type === AST_NODE_TYPES.Literal &&
+          node.right.value === ''
+        ) {
+          context.report({
+            node,
+            messageId: 'unnecessaryCoercion',
+            fix: (fixer): RuleFix[] => [
+              fixer.removeRange([node.range[0], node.left.range[0]]),
+              fixer.removeRange([node.left.range[1], node.range[1]]),
+            ],
+            loc: {
+              start: node.left.loc.end,
+              end: node.loc.end,
+            },
+            data: {
+              violation: "Concatenating a string with ''",
+              type: 'string',
+            },
+          });
+        }
+        if (
+          node.left.type === AST_NODE_TYPES.Literal &&
+          node.left.value === '' &&
+          doesUnderlyingTypeMatchFlag(rightType, ts.TypeFlags.StringLike)
+        ) {
+          context.report({
+            node,
+            messageId: 'unnecessaryCoercion',
+            fix: (fixer): RuleFix[] => [
+              fixer.removeRange([node.range[0], node.right.range[0]]),
+              fixer.removeRange([node.right.range[1], node.range[1]]),
+            ],
+            loc: {
+              start: node.loc.start,
+              end: node.right.loc.start,
+            },
+            data: {
+              violation: "Concatenating '' with a string",
+              type: 'string',
+            },
+          });
+        }
+      },
+      'UnaryExpression[operator = "+"]'(node: TSESTree.UnaryExpression): void {
+        const type = services.getTypeAtLocation(node.argument);
+        if (doesUnderlyingTypeMatchFlag(type, ts.TypeFlags.NumberLike)) {
+          context.report({
+            node,
+            messageId: 'unnecessaryCoercion',
+            fix: (fixer): RuleFix[] => [
+              fixer.removeRange([node.range[0], node.argument.range[0]]),
+              fixer.removeRange([node.argument.range[1], node.range[1]]),
+            ],
+            loc: {
+              start: node.loc.start,
+              end: node.argument.loc.start,
path: packages/eslint-plugin/src/rules/no-unnecessary-coercion.ts
user: login: skondrashov
body: This wonderfully addresses the concerns I had with some of the red underlines I was seeing, with a couple of caveats.

1) You can't add numbers to objects, though I figured out the structure of the object so I'm modifying the column field, and assuming the operator ends on the same line it begins (seems reasonable enough).
2) I opted not to treat !! and ~~ as operators of length 2, because nothing stops a developer from writing something like:
```
! 
    !
              false
```
but I was able to get this underline for a similar case, and all parenthesis/whitespace concerns I had I think are now assuaged:
![image](https://github.com/user-attachments/assets/ddd523d4-30fe-4b8c-86ee-0630bf866756)

See the commit titled "improve loc generation for !!, +, and ~~ operators" for details, keeping in mind that I use `node.parent.loc` for the start and `node.loc` for the end for the `!!` and `~~` operators to achieve the desired effect.
created_at: 2024-11-03T14:11:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,212 @@
+import { RuleTester } from '@typescript-eslint/rule-tester';
+import { AST_NODE_TYPES } from '@typescript-eslint/utils';
+
+import rule from '../../src/rules/no-unnecessary-coercion';
+import { getFixturesRootDir } from '../RuleTester';
+
+const rootDir = getFixturesRootDir();
+
+const ruleTester = new RuleTester({
+  languageOptions: {
+    parserOptions: {
+      tsconfigRootDir: rootDir,
+      project: './tsconfig.json',
+    },
+  },
+});
+
+const messageId = 'unnecessaryCoercion';
path: packages/eslint-plugin/tests/rules/no-unnecessary-coercion.test.ts
user: login: skondrashov
body: changed this since it appears to only be done in a minority of test files, but strongly disagree for the record, as magic constants should be easy to change in one place
created_at: 2024-11-03T14:22:09Z
author_association: CONTRIBUTOR
path: packages/eslint-plugin/src/rules/no-unnecessary-coercion.ts
user: login: skondrashov
body: 1. the rule already has `type: 'suggestion'` (unless I did that in response to this comment and forgot), and it doesn't seem like the API encourages me to move the fix into the `suggest` array
2. in implementing this, it seemed this only makes sense if the thing being type converted is an identifier, so I implemented it only for that case. eg `'asdf' satisfies string` seems besides the point
created_at: 2025-01-26T06:49:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,242 @@
+import type { TSESTree } from '@typescript-eslint/utils';
+import { AST_NODE_TYPES } from '@typescript-eslint/utils';
+import type { RuleFix } from '@typescript-eslint/utils/ts-eslint';
+import * as ts from 'typescript';
+
+import {
+  createRule,
+  getConstrainedTypeAtLocation,
+  getParserServices,
+  isTypeFlagSet,
+} from '../util';
+
+type Options = [];
+type MessageIds = 'unnecessaryCoercion';
+
+export default createRule<Options, MessageIds>({
+  name: 'no-unnecessary-coercion',
+  meta: {
+    docs: {
+      description:
+        'Disallow conversion idioms when they do not change the type or value of the expression',
+      requiresTypeChecking: true,
+    },
+    fixable: 'code',
+    messages: {
+      unnecessaryCoercion:
+        '{{violation}} does not change the type or value of the {{type}}.',
+    },
+    schema: [],
+    type: 'suggestion',
+  },
+  defaultOptions: [],
+  create(context) {
+    function doesUnderlyingTypeMatchFlag(
+      type: ts.Type,
+      typeFlag: ts.TypeFlags,
+    ): boolean {
+      const isString = (t: ts.Type): boolean => {
+        return isTypeFlagSet(t, typeFlag);
+      };
+
+      if (type.isUnion()) {
+        return type.types.every(isString);
+      }
+
+      if (type.isIntersection()) {
+        return type.types.some(isString);
+      }
+
+      return isString(type);
+    }
+
+    const services = getParserServices(context);
+
+    return {
+      CallExpression(node: TSESTree.CallExpression): void {
+        if (
+          node.callee.type === AST_NODE_TYPES.Identifier &&
+          node.arguments.length === 1
+        ) {
+          const type = getConstrainedTypeAtLocation(
+            services,
+            node.arguments[0],
+          );
+
+          if (
+            (doesUnderlyingTypeMatchFlag(type, ts.TypeFlags.StringLike) &&
+              // eslint-disable-next-line @typescript-eslint/internal/prefer-ast-types-enum
+              node.callee.name === 'String') ||
+            (doesUnderlyingTypeMatchFlag(type, ts.TypeFlags.NumberLike) &&
+              node.callee.name === 'Number') ||
+            (doesUnderlyingTypeMatchFlag(type, ts.TypeFlags.BooleanLike) &&
+              // eslint-disable-next-line @typescript-eslint/internal/prefer-ast-types-enum
+              node.callee.name === 'Boolean') ||
+            (doesUnderlyingTypeMatchFlag(type, ts.TypeFlags.BigIntLike) &&
+              node.callee.name === 'BigInt')
+          ) {
+            context.report({
+              node,
+              messageId: 'unnecessaryCoercion',
+              fix: (fixer): RuleFix[] => [
+                fixer.removeRange([node.range[0], node.arguments[0].range[0]]),
+                fixer.removeRange([node.arguments[0].range[1], node.range[1]]),
+              ],
+              data: {
+                violation: `Passing a ${node.callee.name.toLowerCase()} to ${node.callee.name}()`,
+                type: node.callee.name.toLowerCase(),
+              },
+            });
+          }
+        }
+      },
+      'CallExpression > MemberExpression.callee > Identifier[name = "toString"].property'(
+        node: TSESTree.Expression,
+      ): void {
+        const memberExpr = node.parent as TSESTree.MemberExpression;
+        const type = getConstrainedTypeAtLocation(services, memberExpr.object);
+        if (doesUnderlyingTypeMatchFlag(type, ts.TypeFlags.StringLike)) {
+          context.report({
+            node,
+            messageId: 'unnecessaryCoercion',
+            fix: (fixer): RuleFix[] => [
+              fixer.removeRange([
+                memberExpr.parent.range[0],
+                memberExpr.object.range[0],
+              ]),
+              fixer.removeRange([
+                memberExpr.object.range[1],
+                memberExpr.parent.range[1],
+              ]),
+            ],
+            loc: {
+              start: memberExpr.object.loc.end,
+              end: memberExpr.parent.loc.end,
+            },
+            data: {
+              violation: 'Using .toString() on a string',
+              type: 'string',
+            },
+          });
+        }
+      },
+      'AssignmentExpression[operator = "+="], BinaryExpression[operator = "+"]'(
+        node: TSESTree.AssignmentExpression | TSESTree.BinaryExpression,
+      ): void {
+        const leftType = services.getTypeAtLocation(node.left);
+        const rightType = services.getTypeAtLocation(node.right);
+        if (
+          doesUnderlyingTypeMatchFlag(leftType, ts.TypeFlags.StringLike) &&
+          node.right.type === AST_NODE_TYPES.Literal &&
+          node.right.value === ''
+        ) {
+          context.report({
+            node,
+            messageId: 'unnecessaryCoercion',
+            fix: (fixer): RuleFix[] => [
+              fixer.removeRange([node.range[0], node.left.range[0]]),
+              fixer.removeRange([node.left.range[1], node.range[1]]),
+            ],
+            loc: {
+              start: node.left.loc.end,
+              end: node.loc.end,
+            },
+            data: {
+              violation: "Concatenating a string with ''",
+              type: 'string',
+            },
+          });
+        }
+        if (
+          node.left.type === AST_NODE_TYPES.Literal &&
+          node.left.value === '' &&
+          doesUnderlyingTypeMatchFlag(rightType, ts.TypeFlags.StringLike)
+        ) {
+          context.report({
+            node,
+            messageId: 'unnecessaryCoercion',
+            fix: (fixer): RuleFix[] => [
+              fixer.removeRange([node.range[0], node.right.range[0]]),
+              fixer.removeRange([node.right.range[1], node.range[1]]),
+            ],
+            loc: {
+              start: node.loc.start,
+              end: node.right.loc.start,
+            },
+            data: {
+              violation: "Concatenating '' with a string",
+              type: 'string',
+            },
+          });
+        }
+      },
+      'UnaryExpression[operator = "+"]'(node: TSESTree.UnaryExpression): void {
path: packages/eslint-plugin/src/rules/no-unnecessary-coercion.ts
user: login: kirkwaiblinger
body: If we went forward with this, I would certainly draw a hard line at single `+` applied to numeric literal... So, still reporting on the outer `+` of `+(+3)`/`+ +3`, for example. IMO the slope isn't really slippery since something like `+PI` is ambiguous from static analysis as to whether it's a positive or negative number and instead looks more like a mistaken attempt at `Math.abs(PI)` than anything. That said, something like `1-+2` is a big yikes that would be permitted by the naive implementation I have in my head, so... 

i'm fine with leaving it as is 👍. I do agree that your error message wording (and squiggly placement!) makes it totally understandable _why_ the rule is reporting, and it's easy enough for someone to create a feature request for it in the future if there's a want.

Thanks for the discussion!
created_at: 2025-01-29T06:54:21Z
author_association: MEMBER
path: packages/eslint-plugin/src/rules/no-unnecessary-coercion.ts
user: login: kirkwaiblinger
body: > 1. the rule already has `type: 'suggestion'` (unless I did that in response to this comment and forgot)...

This is actually something entirely separate with an unfortunate naming clash, see [the eslint core docs](https://eslint.org/docs/latest/extend/custom-rules#rule-structure). It doesn't affect whether fixes are applied as "fixes" (which we often call "autofixes" to disambiguate) or "suggestion fixes". 

> ...and it doesn't seem like the API encourages me to move the fix into the `suggest` array

Could you clarify what you mean by this? I don't follow. But... yes, we do put suggestion fixers in the report function calls. For a random example, see

https://github.com/typescript-eslint/typescript-eslint/blob/b17c7f25af620c22ab9a27c270c964a6fd29de7c/packages/eslint-plugin/src/rules/no-floating-promises.ts#L192-L204

> 2. in implementing this, it seemed this only makes sense if the thing being type converted is an identifier, so I implemented it only for that case. eg `'asdf' satisfies string` seems besides the point

Especially since this is posited as a suggestion fixer that the user would manually apply, I think we are better off not trying to be clever here. The user can simply disregard the suggestion when it doesn't make sense. 

Identifier is definitely too restrictive since member expressions like `a.b.c` are equally sensible... And if we support those, we need to consider chain expressions `a?.b.c`.

Furthermore, In principle, code like the following might be useful sometimes
```ts
declare function f(): string;

f() satisfies string + "some other string";
```
That way if someone changes `f()` to return a `number` (maybe declared in a whole nother file), they're notified of the issue, instead of getting something like `'42some other string'`. 

In conclusion, while it's true that literals specifically are a little silly to suggest a `satisfies` expression, I think any other syntax is totally fair game, so let's just allow any syntax to trigger this suggestion.
created_at: 2025-01-29T07:11:17Z
author_association: MEMBER
path: packages/eslint-plugin/src/rules/no-unnecessary-coercion.ts
user: login: kirkwaiblinger
body: Looking a bit more at the implementation, I'm assuming you're referring to the fact that, this code is rather awkward:

https://github.com/typescript-eslint/typescript-eslint/blob/606556e809b98decfa190a8ed3890876a5293725/packages/eslint-plugin/src/rules/no-unnecessary-type-conversion.ts#L389-L419

If it helps, feel free to study our util `getFixOrSuggest` and use and/or modify it to be helpful to this use case

https://github.com/typescript-eslint/typescript-eslint/blob/5b2ebcdea9011a6de7e657118874ae85c08d5a0e/packages/eslint-plugin/src/util/getFixOrSuggest.ts#L3-L21

But, i think it's moot since we probably want them all to be suggestions anyway rather than ever applying an autofix
created_at: 2025-01-29T07:20:40Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,212 @@
+import { RuleTester } from '@typescript-eslint/rule-tester';
+import { AST_NODE_TYPES } from '@typescript-eslint/utils';
+
+import rule from '../../src/rules/no-unnecessary-coercion';
+import { getFixturesRootDir } from '../RuleTester';
+
+const rootDir = getFixturesRootDir();
+
+const ruleTester = new RuleTester({
+  languageOptions: {
+    parserOptions: {
+      tsconfigRootDir: rootDir,
+      project: './tsconfig.json',
+    },
+  },
+});
+
+const messageId = 'unnecessaryCoercion';
path: packages/eslint-plugin/tests/rules/no-unnecessary-coercion.test.ts
user: login: kirkwaiblinger
body: > changed this since it appears to only be done in a minority of test files, but strongly disagree for the record, as magic constants should be easy to change in one place

Thanks 🙏. It's true about the magic constants, _but_ this is sort of an idiomatic approach in TS (for better or worse) since the literal types enforce correctness. In other words - this forces you to type the strings out correctly:

https://github.com/typescript-eslint/typescript-eslint/blob/606556e809b98decfa190a8ed3890876a5293725/packages/eslint-plugin/src/rules/no-unnecessary-type-conversion.ts#L19-L21
created_at: 2025-01-29T07:34:16Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,461 @@
+import type { TSESTree } from '@typescript-eslint/utils';
+import type {
+  ReportDescriptorMessageData,
+  RuleFix,
+  RuleFixer,
+} from '@typescript-eslint/utils/ts-eslint';
+
+import { AST_NODE_TYPES } from '@typescript-eslint/utils';
+import * as ts from 'typescript';
+
+import {
+  createRule,
+  getConstrainedTypeAtLocation,
+  getParserServices,
+  isTypeFlagSet,
+} from '../util';
+
+type Options = [];
+type MessageIds =
+  | 'unnecessaryTypeConversion'
+  | 'unnecessaryTypeConversionSuggestion';
+
+export default createRule<Options, MessageIds>({
+  name: 'no-unnecessary-type-conversion',
+  meta: {
+    type: 'suggestion',
+    docs: {
+      description:
+        'Disallow conversion idioms when they do not change the type or value of the expression',
+      requiresTypeChecking: true,
+    },
+    fixable: 'code',
+    hasSuggestions: true,
+    messages: {
+      unnecessaryTypeConversion:
+        '{{violation}} does not change the type or value of the {{type}}.',
+      unnecessaryTypeConversionSuggestion:
+        'Instead, assert that the value satisfies type {{type}}.',
+    },
+    schema: [],
+  },
+  defaultOptions: [],
+  create(context) {
+    function doesUnderlyingTypeMatchFlag(
+      type: ts.Type,
+      typeFlag: ts.TypeFlags,
+    ): boolean {
+      const matchesType = (t: ts.Type): boolean => {
+        return isTypeFlagSet(t, typeFlag);
+      };
+
+      if (type.isUnion()) {
+        return type.types.every(matchesType);
+      }
+
+      if (type.isIntersection()) {
+        return type.types.some(matchesType);
+      }
+
+      return matchesType(type);
+    }
+
+    const doesTypeRequireParentheses = (type: AST_NODE_TYPES) =>
+      ![
+        AST_NODE_TYPES.Literal,
+        AST_NODE_TYPES.Identifier,
+        AST_NODE_TYPES.UnaryExpression,
+        AST_NODE_TYPES.CallExpression,
+        AST_NODE_TYPES.MemberExpression,
+      ].includes(type);
+
+    const shouldHaveSatisfiesSuggestion = (type: AST_NODE_TYPES) =>
+      [AST_NODE_TYPES.Identifier, AST_NODE_TYPES.MemberExpression].includes(
+        type,
+      );
+
+    const services = getParserServices(context);
+    const checker = services.program.getTypeChecker();
+
+    const surroundWithParentheses = (
path: packages/eslint-plugin/src/rules/no-unnecessary-type-conversion.ts
user: login: kirkwaiblinger
body: I'm wondering if this should instead use `getWrappingFixer` or one of the related functions? Or does that not work here?

https://github.com/typescript-eslint/typescript-eslint/blob/3f02687d964c1e7888e5db62256d56cd72c4f281/packages/eslint-plugin/src/util/getWrappingFixer.ts#L29-L78
created_at: 2025-01-29T07:40:23Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,461 @@
+import type { TSESTree } from '@typescript-eslint/utils';
+import type {
+  ReportDescriptorMessageData,
+  RuleFix,
+  RuleFixer,
+} from '@typescript-eslint/utils/ts-eslint';
+
+import { AST_NODE_TYPES } from '@typescript-eslint/utils';
+import * as ts from 'typescript';
+
+import {
+  createRule,
+  getConstrainedTypeAtLocation,
+  getParserServices,
+  isTypeFlagSet,
+} from '../util';
+
+type Options = [];
+type MessageIds =
+  | 'unnecessaryTypeConversion'
+  | 'unnecessaryTypeConversionSuggestion';
+
+export default createRule<Options, MessageIds>({
+  name: 'no-unnecessary-type-conversion',
+  meta: {
+    type: 'suggestion',
+    docs: {
+      description:
+        'Disallow conversion idioms when they do not change the type or value of the expression',
+      requiresTypeChecking: true,
+    },
+    fixable: 'code',
+    hasSuggestions: true,
+    messages: {
+      unnecessaryTypeConversion:
+        '{{violation}} does not change the type or value of the {{type}}.',
+      unnecessaryTypeConversionSuggestion:
+        'Instead, assert that the value satisfies type {{type}}.',
+    },
+    schema: [],
+  },
+  defaultOptions: [],
+  create(context) {
+    function doesUnderlyingTypeMatchFlag(
+      type: ts.Type,
+      typeFlag: ts.TypeFlags,
+    ): boolean {
+      const matchesType = (t: ts.Type): boolean => {
+        return isTypeFlagSet(t, typeFlag);
+      };
+
+      if (type.isUnion()) {
+        return type.types.every(matchesType);
+      }
+
+      if (type.isIntersection()) {
+        return type.types.some(matchesType);
path: packages/eslint-plugin/src/rules/no-unnecessary-type-conversion.ts
user: login: kirkwaiblinger
body: Could you pls ensure that this and other lines lacking coverage are hit by test cases? 🙏 
created_at: 2025-01-29T07:46:42Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,461 @@
+import type { TSESTree } from '@typescript-eslint/utils';
+import type {
+  ReportDescriptorMessageData,
+  RuleFix,
+  RuleFixer,
+} from '@typescript-eslint/utils/ts-eslint';
+
+import { AST_NODE_TYPES } from '@typescript-eslint/utils';
+import * as ts from 'typescript';
+
+import {
+  createRule,
+  getConstrainedTypeAtLocation,
+  getParserServices,
+  isTypeFlagSet,
+} from '../util';
+
+type Options = [];
+type MessageIds =
+  | 'unnecessaryTypeConversion'
+  | 'unnecessaryTypeConversionSuggestion';
+
+export default createRule<Options, MessageIds>({
+  name: 'no-unnecessary-type-conversion',
+  meta: {
+    type: 'suggestion',
+    docs: {
+      description:
+        'Disallow conversion idioms when they do not change the type or value of the expression',
+      requiresTypeChecking: true,
+    },
+    fixable: 'code',
+    hasSuggestions: true,
+    messages: {
+      unnecessaryTypeConversion:
+        '{{violation}} does not change the type or value of the {{type}}.',
+      unnecessaryTypeConversionSuggestion:
+        'Instead, assert that the value satisfies type {{type}}.',
+    },
+    schema: [],
+  },
+  defaultOptions: [],
+  create(context) {
+    function doesUnderlyingTypeMatchFlag(
+      type: ts.Type,
+      typeFlag: ts.TypeFlags,
+    ): boolean {
+      const matchesType = (t: ts.Type): boolean => {
+        return isTypeFlagSet(t, typeFlag);
+      };
+
+      if (type.isUnion()) {
+        return type.types.every(matchesType);
+      }
+
+      if (type.isIntersection()) {
+        return type.types.some(matchesType);
+      }
+
+      return matchesType(type);
+    }
+
+    const doesTypeRequireParentheses = (type: AST_NODE_TYPES) =>
+      ![
+        AST_NODE_TYPES.Literal,
+        AST_NODE_TYPES.Identifier,
+        AST_NODE_TYPES.UnaryExpression,
+        AST_NODE_TYPES.CallExpression,
+        AST_NODE_TYPES.MemberExpression,
+      ].includes(type);
+
+    const shouldHaveSatisfiesSuggestion = (type: AST_NODE_TYPES) =>
+      [AST_NODE_TYPES.Identifier, AST_NODE_TYPES.MemberExpression].includes(
+        type,
+      );
+
+    const services = getParserServices(context);
+    const checker = services.program.getTypeChecker();
+
+    const surroundWithParentheses = (
+      keepParens: boolean,
+      fixer: RuleFixer,
+      range: TSESTree.Range,
+    ) =>
+      keepParens
+        ? [
+            fixer.insertTextBeforeRange(range, '('),
+            fixer.insertTextAfterRange(range, ')'),
+          ]
+        : [];
+    function handleUnaryOperator(
+      node: TSESTree.UnaryExpression,
+      typeFlag: ts.TypeFlags,
+      reportDescriptorMessageData: ReportDescriptorMessageData,
+      isDoubleOperator: boolean, // !! or ~~
+    ) {
+      const type = services.getTypeAtLocation(node.argument);
+      if (doesUnderlyingTypeMatchFlag(type, typeFlag)) {
+        const keepParens = doesTypeRequireParentheses(node.argument.type);
+        const fixFunction = (fixer: RuleFixer): RuleFix[] => [
+          fixer.removeRange([
+            isDoubleOperator ? node.parent.range[0] : node.range[0],
+            node.argument.range[0],
+          ]),
+          fixer.removeRange([
+            node.argument.range[1],
+            isDoubleOperator ? node.parent.range[1] : node.range[1],
+          ]),
+          ...surroundWithParentheses(keepParens, fixer, node.argument.range),
+        ];
+        const typeString = checker.typeToString(type);
+
+        context.report({
+          loc: {
+            start: isDoubleOperator ? node.parent.loc.start : node.loc.start,
+            end: {
+              column: node.loc.start.column + 1,
+              line: node.loc.start.line,
+            },
+          },
+          messageId: 'unnecessaryTypeConversion',
+          data: reportDescriptorMessageData,
+          fix: fixFunction,
+          suggest: shouldHaveSatisfiesSuggestion(node.argument.type)
+            ? [
+                {
+                  messageId: 'unnecessaryTypeConversionSuggestion',
+                  data: { type: reportDescriptorMessageData.type },
+                  fix(fixer): RuleFix[] {
+                    return [
path: packages/eslint-plugin/src/rules/no-unnecessary-type-conversion.ts
user: login: kirkwaiblinger
body: I'm guessing this is being reported as missing coverage because the `fix` takes precedence if present. Let's ensure that we don't provide both (instead giving both options as suggestions)
created_at: 2025-01-29T07:48:31Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,461 @@
+import type { TSESTree } from '@typescript-eslint/utils';
+import type {
+  ReportDescriptorMessageData,
+  RuleFix,
+  RuleFixer,
+} from '@typescript-eslint/utils/ts-eslint';
+
+import { AST_NODE_TYPES } from '@typescript-eslint/utils';
+import * as ts from 'typescript';
+
+import {
+  createRule,
+  getConstrainedTypeAtLocation,
+  getParserServices,
+  isTypeFlagSet,
+} from '../util';
+
+type Options = [];
+type MessageIds =
+  | 'unnecessaryTypeConversion'
+  | 'unnecessaryTypeConversionSuggestion';
+
+export default createRule<Options, MessageIds>({
+  name: 'no-unnecessary-type-conversion',
+  meta: {
+    type: 'suggestion',
+    docs: {
+      description:
+        'Disallow conversion idioms when they do not change the type or value of the expression',
+      requiresTypeChecking: true,
+    },
+    fixable: 'code',
+    hasSuggestions: true,
+    messages: {
+      unnecessaryTypeConversion:
+        '{{violation}} does not change the type or value of the {{type}}.',
+      unnecessaryTypeConversionSuggestion:
+        'Instead, assert that the value satisfies type {{type}}.',
+    },
+    schema: [],
+  },
+  defaultOptions: [],
+  create(context) {
+    function doesUnderlyingTypeMatchFlag(
+      type: ts.Type,
+      typeFlag: ts.TypeFlags,
+    ): boolean {
+      const matchesType = (t: ts.Type): boolean => {
+        return isTypeFlagSet(t, typeFlag);
+      };
+
+      if (type.isUnion()) {
+        return type.types.every(matchesType);
+      }
+
+      if (type.isIntersection()) {
+        return type.types.some(matchesType);
+      }
+
+      return matchesType(type);
+    }
+
+    const doesTypeRequireParentheses = (type: AST_NODE_TYPES) =>
+      ![
+        AST_NODE_TYPES.Literal,
+        AST_NODE_TYPES.Identifier,
+        AST_NODE_TYPES.UnaryExpression,
+        AST_NODE_TYPES.CallExpression,
+        AST_NODE_TYPES.MemberExpression,
+      ].includes(type);
+
+    const shouldHaveSatisfiesSuggestion = (type: AST_NODE_TYPES) =>
+      [AST_NODE_TYPES.Identifier, AST_NODE_TYPES.MemberExpression].includes(
+        type,
+      );
+
+    const services = getParserServices(context);
+    const checker = services.program.getTypeChecker();
+
+    const surroundWithParentheses = (
path: packages/eslint-plugin/src/rules/no-unnecessary-type-conversion.ts
user: login: skondrashov
body: For the `satisfies` suggestions, this seems to work really well. For the regular fix, it works, but it's weird cause you have to pass a no-op as the `wrap` parameter, and it does some redundant logic internally, and puts some parentheses in places they don't need to be, in some cases double sets of parentheses.

The code is a whole lot cleaner using `getWrappingFixer` for everything, but it's a little strange for the case where nothing is actually wrapping the code. I'm tempted to modify `getWrappingFixer` to make the `wrap` parameter optional, but then it's not really a "wrapping" fixer at all, more an unwrapping fixer. Would like your thoughts, but I've pushed up the code that uses `getWrappingFixer` for everything as well as a very rudimentary change to the API that makes `wrap` optional.
created_at: 2025-02-02T19:56:18Z
author_association: CONTRIBUTOR
path: packages/eslint-plugin/src/rules/no-unnecessary-coercion.ts
user: login: skondrashov
body: The awkwardness in the API that I see is that you're forced to provide `messageId` and `data` even if there's no fix to be applied, so you have to duplicate those fields in the suggestion fix.

So, you get something like:
```typescript
context.report({
  node,
  messageId,
  data,
  suggest: [
    {
      messageId,
      data,
      fix: someFixer,
    },
  ],
});
```

rather than the cleaner:

```typescript
context.report({
  node,
  messageId,
  data,
  fix: someFixer,
});
```

To be honest, I don't understand the motivation for this, though the only thing stopping me otherwise is how verbose it is. Is the point that the fix can be dangerous when working with types that are inaccurate, so it's reckless to autofix it?
created_at: 2025-02-02T20:33:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,369 @@
+import type { TSESTree } from '@typescript-eslint/utils';
+import type {
+  ReportDescriptorMessageData,
+  RuleFix,
+  RuleFixer,
+} from '@typescript-eslint/utils/ts-eslint';
+
+import { AST_NODE_TYPES } from '@typescript-eslint/utils';
+import * as ts from 'typescript';
+
+import {
+  createRule,
+  getConstrainedTypeAtLocation,
+  getParserServices,
+  getWrappingFixer,
+  isTypeFlagSet,
+} from '../util';
+
+type Options = [];
+type MessageIds =
+  | 'unnecessaryTypeConversion'
+  | 'unnecessaryTypeConversionSuggestion';
+
+export default createRule<Options, MessageIds>({
+  name: 'no-unnecessary-type-conversion',
+  meta: {
+    type: 'suggestion',
+    docs: {
+      description:
+        'Disallow conversion idioms when they do not change the type or value of the expression',
+      requiresTypeChecking: true,
+    },
+    fixable: 'code',
+    hasSuggestions: true,
+    messages: {
+      unnecessaryTypeConversion:
+        '{{violation}} does not change the type or value of the {{type}}.',
+      unnecessaryTypeConversionSuggestion:
+        'Instead, assert that the value satisfies type {{type}}.',
+    },
+    schema: [],
+  },
+  defaultOptions: [],
+  create(context) {
+    function doesUnderlyingTypeMatchFlag(
+      type: ts.Type,
+      typeFlag: ts.TypeFlags,
+    ): boolean {
+      const matchesType = (t: ts.Type): boolean => {
+        return isTypeFlagSet(t, typeFlag);
+      };
+
+      if (type.isUnion()) {
+        return type.types.every(matchesType);
+      }
+
+      return matchesType(type);
+    }
path: packages/eslint-plugin/src/rules/no-unnecessary-type-conversion.ts
user: login: kirkwaiblinger
body: We have a helper for this:
```suggestion
    function doesUnderlyingTypeMatchFlag(
      type: ts.Type,
      typeFlag: ts.TypeFlags,
    ): boolean {
      return unionTypeParts(type).every(t => tsutils.isTypeFlagSet(t, typeFlag));
    }
```
created_at: 2025-02-10T01:59:33Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,369 @@
+import type { TSESTree } from '@typescript-eslint/utils';
+import type {
+  ReportDescriptorMessageData,
+  RuleFix,
+  RuleFixer,
+} from '@typescript-eslint/utils/ts-eslint';
+
+import { AST_NODE_TYPES } from '@typescript-eslint/utils';
+import * as ts from 'typescript';
+
+import {
+  createRule,
+  getConstrainedTypeAtLocation,
+  getParserServices,
+  getWrappingFixer,
+  isTypeFlagSet,
+} from '../util';
+
+type Options = [];
+type MessageIds =
+  | 'unnecessaryTypeConversion'
+  | 'unnecessaryTypeConversionSuggestion';
+
+export default createRule<Options, MessageIds>({
+  name: 'no-unnecessary-type-conversion',
+  meta: {
+    type: 'suggestion',
+    docs: {
+      description:
+        'Disallow conversion idioms when they do not change the type or value of the expression',
+      requiresTypeChecking: true,
+    },
+    fixable: 'code',
+    hasSuggestions: true,
+    messages: {
+      unnecessaryTypeConversion:
+        '{{violation}} does not change the type or value of the {{type}}.',
+      unnecessaryTypeConversionSuggestion:
+        'Instead, assert that the value satisfies type {{type}}.',
+    },
+    schema: [],
+  },
+  defaultOptions: [],
+  create(context) {
+    function doesUnderlyingTypeMatchFlag(
+      type: ts.Type,
+      typeFlag: ts.TypeFlags,
+    ): boolean {
+      const matchesType = (t: ts.Type): boolean => {
+        return isTypeFlagSet(t, typeFlag);
+      };
+
+      if (type.isUnion()) {
+        return type.types.every(matchesType);
+      }
+
+      return matchesType(type);
+    }
+
+    const services = getParserServices(context);
+
+    function handleUnaryOperator(
+      node: TSESTree.UnaryExpression,
+      typeFlag: ts.TypeFlags,
+      reportDescriptorMessageData: ReportDescriptorMessageData,
+      isDoubleOperator: boolean, // !! or ~~
+    ) {
+      const type = services.getTypeAtLocation(node.argument);
+      if (doesUnderlyingTypeMatchFlag(type, typeFlag)) {
+        const wrappingFixerParams = {
+          node: isDoubleOperator ? node.parent : node,
+          innerNode: [node.argument],
+          sourceCode: context.sourceCode,
+        };
+        const typeString =
+          typeFlag === ts.TypeFlags.BooleanLike ? 'boolean' : 'number';
+
+        context.report({
+          loc: {
+            start: isDoubleOperator ? node.parent.loc.start : node.loc.start,
+            end: {
+              column: node.loc.start.column + 1,
+              line: node.loc.start.line,
+            },
+          },
+          messageId: 'unnecessaryTypeConversion',
+          data: reportDescriptorMessageData,
+          fix: getWrappingFixer(wrappingFixerParams),
+          suggest: [
+            {
+              messageId: 'unnecessaryTypeConversionSuggestion',
+              data: { type: reportDescriptorMessageData.type },
+              fix: getWrappingFixer({
+                ...wrappingFixerParams,
+                wrap: expr => `${expr} satisfies ${typeString}`,
+              }),
+            },
+          ],
+        });
+      }
+    }
+
+    return {
+      'AssignmentExpression[operator = "+="]'(
+        node: TSESTree.AssignmentExpression,
+      ): void {
+        if (
+          node.right.type === AST_NODE_TYPES.Literal &&
+          node.right.value === '' &&
+          doesUnderlyingTypeMatchFlag(
+            services.getTypeAtLocation(node.left),
+            ts.TypeFlags.StringLike,
+          )
+        ) {
+          const wrappingFixerParams = {
+            node,
+            innerNode: [node.left],
+            sourceCode: context.sourceCode,
+          };
+
+          context.report({
+            node,
+            messageId: 'unnecessaryTypeConversion',
+            data: {
+              type: 'string',
+              violation: "Concatenating a string with ''",
+            },
+            fix:
+              node.parent.type === AST_NODE_TYPES.ExpressionStatement
+                ? (fixer: RuleFixer): RuleFix[] => [
+                    fixer.removeRange([
+                      node.parent.range[0],
+                      node.parent.range[1],
+                    ]),
+                  ]
+                : getWrappingFixer(wrappingFixerParams),
+            suggest: [
+              {
+                messageId: 'unnecessaryTypeConversionSuggestion',
+                data: { type: 'string' },
+                fix: getWrappingFixer({
+                  ...wrappingFixerParams,
+                  wrap: expr => `${expr} satisfies string`,
+                }),
+              },
+            ],
+          });
+        }
+      },
+      'BinaryExpression[operator = "+"]'(
+        node: TSESTree.BinaryExpression,
+      ): void {
+        if (
+          node.right.type === AST_NODE_TYPES.Literal &&
+          node.right.value === '' &&
+          doesUnderlyingTypeMatchFlag(
+            services.getTypeAtLocation(node.left),
+            ts.TypeFlags.StringLike,
+          )
+        ) {
+          const wrappingFixerParams = {
+            node,
+            innerNode: [node.left],
+            sourceCode: context.sourceCode,
+          };
+
+          context.report({
+            loc: {
+              start: node.left.loc.end,
+              end: node.loc.end,
+            },
+            messageId: 'unnecessaryTypeConversion',
+            data: {
+              type: 'string',
+              violation: "Concatenating a string with ''",
+            },
+            fix: getWrappingFixer(wrappingFixerParams),
+            suggest: [
+              {
+                messageId: 'unnecessaryTypeConversionSuggestion',
+                data: { type: 'string' },
+                fix: getWrappingFixer({
+                  ...wrappingFixerParams,
+                  wrap: expr => `${expr} satisfies string`,
+                }),
+              },
+            ],
+          });
+        } else if (
+          node.left.type === AST_NODE_TYPES.Literal &&
+          node.left.value === '' &&
+          doesUnderlyingTypeMatchFlag(
+            services.getTypeAtLocation(node.right),
+            ts.TypeFlags.StringLike,
+          )
+        ) {
+          const wrappingFixerParams = {
+            node,
+            innerNode: [node.right],
+            sourceCode: context.sourceCode,
+          };
+
+          context.report({
+            loc: {
+              start: node.loc.start,
+              end: node.right.loc.start,
+            },
+            messageId: 'unnecessaryTypeConversion',
+            data: {
+              type: 'string',
+              violation: "Concatenating '' with a string",
+            },
+            fix: getWrappingFixer(wrappingFixerParams),
+            suggest: [
+              {
+                messageId: 'unnecessaryTypeConversionSuggestion',
+                data: { type: 'string' },
+                fix: getWrappingFixer({
+                  ...wrappingFixerParams,
+                  wrap: expr => `${expr} satisfies string`,
+                }),
+              },
+            ],
+          });
+        }
+      },
+      CallExpression(node: TSESTree.CallExpression): void {
+        if (
+          node.callee.type === AST_NODE_TYPES.Identifier &&
+          node.arguments.length === 1
+        ) {
+          const getType = () =>
+            getConstrainedTypeAtLocation(services, node.arguments[0]);
+
+          const isBuiltInCall = (name: string) => {
+            if ((node.callee as TSESTree.Identifier).name === name) {
+              const scope = context.sourceCode.getScope(node);
+              const variable = scope.set.get(name);
+              return !variable?.defs.length;
+            }
+            return false;
+          };
+
+          if (
+            // eslint-disable-next-line @typescript-eslint/internal/prefer-ast-types-enum
+            (isBuiltInCall('String') &&
+              doesUnderlyingTypeMatchFlag(
+                getType(),
+                ts.TypeFlags.StringLike,
+              )) ||
+            (isBuiltInCall('Number') &&
+              doesUnderlyingTypeMatchFlag(
+                getType(),
+                ts.TypeFlags.NumberLike,
+              )) ||
+            // eslint-disable-next-line @typescript-eslint/internal/prefer-ast-types-enum
+            (isBuiltInCall('Boolean') &&
+              doesUnderlyingTypeMatchFlag(
+                getType(),
+                ts.TypeFlags.BooleanLike,
+              )) ||
+            (isBuiltInCall('BigInt') &&
+              doesUnderlyingTypeMatchFlag(getType(), ts.TypeFlags.BigIntLike))
+          ) {
+            const wrappingFixerParams = {
+              node,
+              innerNode: [node.arguments[0]],
+              sourceCode: context.sourceCode,
+            };
+            const typeString = node.callee.name.toLowerCase();
+
+            context.report({
+              loc: node.callee.loc,
+              messageId: 'unnecessaryTypeConversion',
+              data: {
+                type: node.callee.name.toLowerCase(),
+                violation: `Passing a ${typeString} to ${node.callee.name}()`,
+              },
+              fix: getWrappingFixer(wrappingFixerParams),
+              suggest: [
+                {
+                  messageId: 'unnecessaryTypeConversionSuggestion',
+                  data: { type: typeString },
+                  fix: getWrappingFixer({
+                    ...wrappingFixerParams,
+                    wrap: expr => `${expr} satisfies ${typeString}`,
+                  }),
+                },
+              ],
+            });
+          }
+        }
+      },
+      'CallExpression > MemberExpression.callee > Identifier[name = "toString"].property'(
+        node: TSESTree.Expression,
+      ): void {
+        const memberExpr = node.parent as TSESTree.MemberExpression;
+        const type = getConstrainedTypeAtLocation(services, memberExpr.object);
+        if (doesUnderlyingTypeMatchFlag(type, ts.TypeFlags.StringLike)) {
+          const wrappingFixerParams = {
+            node: memberExpr.parent,
+            innerNode: [memberExpr.object],
+            sourceCode: context.sourceCode,
+          };
+
+          context.report({
+            loc: {
+              start: memberExpr.property.loc.start,
+              end: memberExpr.parent.loc.end,
+            },
+            messageId: 'unnecessaryTypeConversion',
+            data: {
+              type: 'string',
+              violation: "Calling a string's .toString() method",
+            },
+            fix: getWrappingFixer(wrappingFixerParams),
+            suggest: [
+              {
+                messageId: 'unnecessaryTypeConversionSuggestion',
+                data: { type: 'string' },
+                fix: getWrappingFixer({
+                  ...wrappingFixerParams,
+                  wrap: expr => `${expr} satisfies string`,
+                }),
+              },
+            ],
+          });
+        }
+      },
+      'UnaryExpression[operator = "!"] > UnaryExpression[operator = "!"]'(
+        node: TSESTree.UnaryExpression,
+      ): void {
+        handleUnaryOperator(
+          node,
+          ts.TypeFlags.BooleanLike,
+          {
+            type: 'boolean',
+            violation: 'Using !! on a boolean',
+          },
+          true,
+        );
+      },
+      'UnaryExpression[operator = "+"]'(node: TSESTree.UnaryExpression): void {
+        handleUnaryOperator(
+          node,
+          ts.TypeFlags.NumberLike,
+          {
+            type: 'number',
+            violation: 'Using the unary + operator on a number',
+          },
+          false,
+        );
+      },
+      'UnaryExpression[operator = "~"] > UnaryExpression[operator = "~"]'(
+        node: TSESTree.UnaryExpression,
+      ): void {
+        handleUnaryOperator(
+          node,
+          ts.TypeFlags.NumberLike,
+          {
+            type: 'boolean',
path: packages/eslint-plugin/src/rules/no-unnecessary-type-conversion.ts
user: login: kirkwaiblinger
body: ```suggestion
            type: 'number',
```
created_at: 2025-02-10T02:01:09Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,369 @@
+import type { TSESTree } from '@typescript-eslint/utils';
+import type {
+  ReportDescriptorMessageData,
+  RuleFix,
+  RuleFixer,
+} from '@typescript-eslint/utils/ts-eslint';
+
+import { AST_NODE_TYPES } from '@typescript-eslint/utils';
+import * as ts from 'typescript';
+
+import {
+  createRule,
+  getConstrainedTypeAtLocation,
+  getParserServices,
+  getWrappingFixer,
+  isTypeFlagSet,
+} from '../util';
+
+type Options = [];
+type MessageIds =
+  | 'unnecessaryTypeConversion'
+  | 'unnecessaryTypeConversionSuggestion';
+
+export default createRule<Options, MessageIds>({
+  name: 'no-unnecessary-type-conversion',
+  meta: {
+    type: 'suggestion',
+    docs: {
+      description:
+        'Disallow conversion idioms when they do not change the type or value of the expression',
+      requiresTypeChecking: true,
+    },
+    fixable: 'code',
+    hasSuggestions: true,
+    messages: {
+      unnecessaryTypeConversion:
+        '{{violation}} does not change the type or value of the {{type}}.',
+      unnecessaryTypeConversionSuggestion:
+        'Instead, assert that the value satisfies type {{type}}.',
+    },
+    schema: [],
+  },
+  defaultOptions: [],
+  create(context) {
+    function doesUnderlyingTypeMatchFlag(
+      type: ts.Type,
+      typeFlag: ts.TypeFlags,
+    ): boolean {
+      const matchesType = (t: ts.Type): boolean => {
+        return isTypeFlagSet(t, typeFlag);
+      };
+
+      if (type.isUnion()) {
+        return type.types.every(matchesType);
+      }
+
+      return matchesType(type);
+    }
+
+    const services = getParserServices(context);
+
+    function handleUnaryOperator(
+      node: TSESTree.UnaryExpression,
+      typeFlag: ts.TypeFlags,
+      reportDescriptorMessageData: ReportDescriptorMessageData,
+      isDoubleOperator: boolean, // !! or ~~
+    ) {
+      const type = services.getTypeAtLocation(node.argument);
+      if (doesUnderlyingTypeMatchFlag(type, typeFlag)) {
+        const wrappingFixerParams = {
+          node: isDoubleOperator ? node.parent : node,
+          innerNode: [node.argument],
+          sourceCode: context.sourceCode,
+        };
+        const typeString =
path: packages/eslint-plugin/src/rules/no-unnecessary-type-conversion.ts
user: login: kirkwaiblinger
body: This assumes knowledge of its caller, and doesn't really seem to be necessary..... I was playing around and refactored a couple things, WDYT?
- removed `typeString` deduction
- provide report `data` explicitly (probably best to do since the report data have very loose types)
- remove duplication of the `isDoubleOperator` deduction with `outerNode` variable (which tbh might be better off coming from the caller as well).

```ts
    function handleUnaryOperator(
      node: TSESTree.UnaryExpression,
      typeFlag: ts.TypeFlags,
      isDoubleOperator: boolean, // !! or ~~
      typeString: string,
      violation: string,
    ) {
      const outerNode = isDoubleOperator ? node.parent : node;
      const type = services.getTypeAtLocation(node.argument);
      if (doesUnderlyingTypeMatchFlag(type, typeFlag)) {
        context.report({
          loc: {
            start: outerNode.loc.start,
            end: {
              column: node.loc.start.column + 1,
              line: node.loc.start.line,
            },
          },
          messageId: 'unnecessaryTypeConversion',
          data: {
            type: typeString,
            violation,
          },
          fix: getWrappingFixer({
            node: outerNode,
            innerNode: [node.argument],
            sourceCode: context.sourceCode,
          }),
          suggest: [
            {
              messageId: 'unnecessaryTypeConversionSuggestion',
              data: { type: typeString },
              fix: getWrappingFixer({
                node: outerNode,
                innerNode: [node.argument],
                sourceCode: context.sourceCode,
                wrap: expr => `${expr} satisfies ${typeString}`,
              }),
            },
          ],
        });
      }
    }
```
created_at: 2025-02-10T02:19:38Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,369 @@
+import type { TSESTree } from '@typescript-eslint/utils';
+import type {
+  ReportDescriptorMessageData,
+  RuleFix,
+  RuleFixer,
+} from '@typescript-eslint/utils/ts-eslint';
+
+import { AST_NODE_TYPES } from '@typescript-eslint/utils';
+import * as ts from 'typescript';
+
+import {
+  createRule,
+  getConstrainedTypeAtLocation,
+  getParserServices,
+  getWrappingFixer,
+  isTypeFlagSet,
+} from '../util';
+
+type Options = [];
+type MessageIds =
+  | 'unnecessaryTypeConversion'
+  | 'unnecessaryTypeConversionSuggestion';
+
+export default createRule<Options, MessageIds>({
+  name: 'no-unnecessary-type-conversion',
+  meta: {
+    type: 'suggestion',
+    docs: {
+      description:
+        'Disallow conversion idioms when they do not change the type or value of the expression',
+      requiresTypeChecking: true,
+    },
+    fixable: 'code',
+    hasSuggestions: true,
+    messages: {
+      unnecessaryTypeConversion:
+        '{{violation}} does not change the type or value of the {{type}}.',
+      unnecessaryTypeConversionSuggestion:
+        'Instead, assert that the value satisfies type {{type}}.',
+    },
+    schema: [],
+  },
+  defaultOptions: [],
+  create(context) {
+    function doesUnderlyingTypeMatchFlag(
+      type: ts.Type,
+      typeFlag: ts.TypeFlags,
+    ): boolean {
+      const matchesType = (t: ts.Type): boolean => {
+        return isTypeFlagSet(t, typeFlag);
+      };
+
+      if (type.isUnion()) {
+        return type.types.every(matchesType);
+      }
+
+      return matchesType(type);
+    }
+
+    const services = getParserServices(context);
+
+    function handleUnaryOperator(
+      node: TSESTree.UnaryExpression,
+      typeFlag: ts.TypeFlags,
+      reportDescriptorMessageData: ReportDescriptorMessageData,
+      isDoubleOperator: boolean, // !! or ~~
+    ) {
+      const type = services.getTypeAtLocation(node.argument);
+      if (doesUnderlyingTypeMatchFlag(type, typeFlag)) {
+        const wrappingFixerParams = {
+          node: isDoubleOperator ? node.parent : node,
+          innerNode: [node.argument],
+          sourceCode: context.sourceCode,
+        };
+        const typeString =
+          typeFlag === ts.TypeFlags.BooleanLike ? 'boolean' : 'number';
+
+        context.report({
path: packages/eslint-plugin/src/rules/no-unnecessary-type-conversion.ts
user: login: kirkwaiblinger
body: Here and elsewhere, for the fixes -> suggestion change, you'll want to do:

```ts
        context.report({
          loc: /* etc */,
          messageId: 'unnecessaryTypeConversion',
          data: {
             /* etc */
          },
          suggest: [
            {
              // new suggestion message to be created
              messageId: 'suggestRemoveConversion',
              fix: /* etc */,
            },
            {
              // renamed from 'unnecessaryTypeConversionSuggestion'
              messageId: 'suggestSatisfies',
              data: { type: typeString },
              fix: /* etc */,
            },
          ],
        });
```
created_at: 2025-02-10T02:36:16Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,369 @@
+import type { TSESTree } from '@typescript-eslint/utils';
+import type {
+  ReportDescriptorMessageData,
+  RuleFix,
+  RuleFixer,
+} from '@typescript-eslint/utils/ts-eslint';
+
+import { AST_NODE_TYPES } from '@typescript-eslint/utils';
+import * as ts from 'typescript';
+
+import {
+  createRule,
+  getConstrainedTypeAtLocation,
+  getParserServices,
+  getWrappingFixer,
+  isTypeFlagSet,
+} from '../util';
+
+type Options = [];
+type MessageIds =
+  | 'unnecessaryTypeConversion'
+  | 'unnecessaryTypeConversionSuggestion';
+
+export default createRule<Options, MessageIds>({
+  name: 'no-unnecessary-type-conversion',
+  meta: {
+    type: 'suggestion',
+    docs: {
+      description:
+        'Disallow conversion idioms when they do not change the type or value of the expression',
+      requiresTypeChecking: true,
+    },
+    fixable: 'code',
+    hasSuggestions: true,
+    messages: {
+      unnecessaryTypeConversion:
+        '{{violation}} does not change the type or value of the {{type}}.',
+      unnecessaryTypeConversionSuggestion:
+        'Instead, assert that the value satisfies type {{type}}.',
+    },
+    schema: [],
+  },
+  defaultOptions: [],
+  create(context) {
+    function doesUnderlyingTypeMatchFlag(
+      type: ts.Type,
+      typeFlag: ts.TypeFlags,
+    ): boolean {
+      const matchesType = (t: ts.Type): boolean => {
+        return isTypeFlagSet(t, typeFlag);
+      };
+
+      if (type.isUnion()) {
+        return type.types.every(matchesType);
+      }
+
+      return matchesType(type);
+    }
+
+    const services = getParserServices(context);
+
+    function handleUnaryOperator(
+      node: TSESTree.UnaryExpression,
+      typeFlag: ts.TypeFlags,
+      reportDescriptorMessageData: ReportDescriptorMessageData,
+      isDoubleOperator: boolean, // !! or ~~
+    ) {
+      const type = services.getTypeAtLocation(node.argument);
+      if (doesUnderlyingTypeMatchFlag(type, typeFlag)) {
+        const wrappingFixerParams = {
+          node: isDoubleOperator ? node.parent : node,
+          innerNode: [node.argument],
+          sourceCode: context.sourceCode,
+        };
+        const typeString =
+          typeFlag === ts.TypeFlags.BooleanLike ? 'boolean' : 'number';
+
+        context.report({
+          loc: {
+            start: isDoubleOperator ? node.parent.loc.start : node.loc.start,
+            end: {
+              column: node.loc.start.column + 1,
+              line: node.loc.start.line,
+            },
+          },
+          messageId: 'unnecessaryTypeConversion',
+          data: reportDescriptorMessageData,
+          fix: getWrappingFixer(wrappingFixerParams),
+          suggest: [
+            {
+              messageId: 'unnecessaryTypeConversionSuggestion',
+              data: { type: reportDescriptorMessageData.type },
+              fix: getWrappingFixer({
+                ...wrappingFixerParams,
+                wrap: expr => `${expr} satisfies ${typeString}`,
+              }),
+            },
+          ],
+        });
+      }
+    }
+
+    return {
+      'AssignmentExpression[operator = "+="]'(
+        node: TSESTree.AssignmentExpression,
+      ): void {
+        if (
+          node.right.type === AST_NODE_TYPES.Literal &&
+          node.right.value === '' &&
+          doesUnderlyingTypeMatchFlag(
+            services.getTypeAtLocation(node.left),
+            ts.TypeFlags.StringLike,
+          )
+        ) {
+          const wrappingFixerParams = {
+            node,
+            innerNode: [node.left],
+            sourceCode: context.sourceCode,
+          };
+
+          context.report({
+            node,
+            messageId: 'unnecessaryTypeConversion',
+            data: {
+              type: 'string',
+              violation: "Concatenating a string with ''",
+            },
+            fix:
+              node.parent.type === AST_NODE_TYPES.ExpressionStatement
+                ? (fixer: RuleFixer): RuleFix[] => [
+                    fixer.removeRange([
+                      node.parent.range[0],
+                      node.parent.range[1],
+                    ]),
+                  ]
+                : getWrappingFixer(wrappingFixerParams),
+            suggest: [
+              {
+                messageId: 'unnecessaryTypeConversionSuggestion',
+                data: { type: 'string' },
+                fix: getWrappingFixer({
+                  ...wrappingFixerParams,
+                  wrap: expr => `${expr} satisfies string`,
+                }),
+              },
+            ],
+          });
+        }
+      },
+      'BinaryExpression[operator = "+"]'(
+        node: TSESTree.BinaryExpression,
+      ): void {
+        if (
+          node.right.type === AST_NODE_TYPES.Literal &&
+          node.right.value === '' &&
+          doesUnderlyingTypeMatchFlag(
+            services.getTypeAtLocation(node.left),
+            ts.TypeFlags.StringLike,
+          )
+        ) {
+          const wrappingFixerParams = {
+            node,
+            innerNode: [node.left],
+            sourceCode: context.sourceCode,
+          };
+
+          context.report({
+            loc: {
+              start: node.left.loc.end,
+              end: node.loc.end,
+            },
+            messageId: 'unnecessaryTypeConversion',
+            data: {
+              type: 'string',
+              violation: "Concatenating a string with ''",
+            },
+            fix: getWrappingFixer(wrappingFixerParams),
+            suggest: [
+              {
+                messageId: 'unnecessaryTypeConversionSuggestion',
+                data: { type: 'string' },
+                fix: getWrappingFixer({
+                  ...wrappingFixerParams,
+                  wrap: expr => `${expr} satisfies string`,
+                }),
+              },
+            ],
+          });
+        } else if (
+          node.left.type === AST_NODE_TYPES.Literal &&
+          node.left.value === '' &&
+          doesUnderlyingTypeMatchFlag(
+            services.getTypeAtLocation(node.right),
+            ts.TypeFlags.StringLike,
+          )
+        ) {
+          const wrappingFixerParams = {
+            node,
+            innerNode: [node.right],
+            sourceCode: context.sourceCode,
+          };
+
+          context.report({
+            loc: {
+              start: node.loc.start,
+              end: node.right.loc.start,
+            },
+            messageId: 'unnecessaryTypeConversion',
+            data: {
+              type: 'string',
+              violation: "Concatenating '' with a string",
+            },
+            fix: getWrappingFixer(wrappingFixerParams),
+            suggest: [
+              {
+                messageId: 'unnecessaryTypeConversionSuggestion',
+                data: { type: 'string' },
+                fix: getWrappingFixer({
+                  ...wrappingFixerParams,
+                  wrap: expr => `${expr} satisfies string`,
+                }),
+              },
+            ],
+          });
+        }
+      },
+      CallExpression(node: TSESTree.CallExpression): void {
+        if (
+          node.callee.type === AST_NODE_TYPES.Identifier &&
+          node.arguments.length === 1
+        ) {
+          const getType = () =>
path: packages/eslint-plugin/src/rules/no-unnecessary-type-conversion.ts
user: login: kirkwaiblinger
body: Nice way of lazily fetching the type info! 🙂 

Could you put a comment or update the name to self-document that intention? Whatever you think is best!

For example,
```suggestion
          // get type info lazily
          const getType = () =>
```
or 
```suggestion
          const getTypeLazy = () =>
```

🤷 
created_at: 2025-02-10T02:44:59Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,369 @@
+import type { TSESTree } from '@typescript-eslint/utils';
+import type {
+  ReportDescriptorMessageData,
+  RuleFix,
+  RuleFixer,
+} from '@typescript-eslint/utils/ts-eslint';
+
+import { AST_NODE_TYPES } from '@typescript-eslint/utils';
+import * as ts from 'typescript';
+
+import {
+  createRule,
+  getConstrainedTypeAtLocation,
+  getParserServices,
+  getWrappingFixer,
+  isTypeFlagSet,
+} from '../util';
+
+type Options = [];
+type MessageIds =
+  | 'unnecessaryTypeConversion'
+  | 'unnecessaryTypeConversionSuggestion';
+
+export default createRule<Options, MessageIds>({
+  name: 'no-unnecessary-type-conversion',
+  meta: {
+    type: 'suggestion',
+    docs: {
+      description:
+        'Disallow conversion idioms when they do not change the type or value of the expression',
+      requiresTypeChecking: true,
+    },
+    fixable: 'code',
+    hasSuggestions: true,
+    messages: {
+      unnecessaryTypeConversion:
+        '{{violation}} does not change the type or value of the {{type}}.',
+      unnecessaryTypeConversionSuggestion:
+        'Instead, assert that the value satisfies type {{type}}.',
+    },
+    schema: [],
+  },
+  defaultOptions: [],
+  create(context) {
+    function doesUnderlyingTypeMatchFlag(
+      type: ts.Type,
+      typeFlag: ts.TypeFlags,
+    ): boolean {
+      const matchesType = (t: ts.Type): boolean => {
+        return isTypeFlagSet(t, typeFlag);
+      };
+
+      if (type.isUnion()) {
+        return type.types.every(matchesType);
+      }
+
+      return matchesType(type);
+    }
+
+    const services = getParserServices(context);
+
+    function handleUnaryOperator(
+      node: TSESTree.UnaryExpression,
+      typeFlag: ts.TypeFlags,
+      reportDescriptorMessageData: ReportDescriptorMessageData,
+      isDoubleOperator: boolean, // !! or ~~
+    ) {
+      const type = services.getTypeAtLocation(node.argument);
+      if (doesUnderlyingTypeMatchFlag(type, typeFlag)) {
+        const wrappingFixerParams = {
+          node: isDoubleOperator ? node.parent : node,
+          innerNode: [node.argument],
+          sourceCode: context.sourceCode,
+        };
+        const typeString =
+          typeFlag === ts.TypeFlags.BooleanLike ? 'boolean' : 'number';
+
+        context.report({
+          loc: {
+            start: isDoubleOperator ? node.parent.loc.start : node.loc.start,
+            end: {
+              column: node.loc.start.column + 1,
+              line: node.loc.start.line,
+            },
+          },
+          messageId: 'unnecessaryTypeConversion',
+          data: reportDescriptorMessageData,
+          fix: getWrappingFixer(wrappingFixerParams),
+          suggest: [
+            {
+              messageId: 'unnecessaryTypeConversionSuggestion',
+              data: { type: reportDescriptorMessageData.type },
+              fix: getWrappingFixer({
+                ...wrappingFixerParams,
+                wrap: expr => `${expr} satisfies ${typeString}`,
+              }),
+            },
+          ],
+        });
+      }
+    }
+
+    return {
+      'AssignmentExpression[operator = "+="]'(
+        node: TSESTree.AssignmentExpression,
+      ): void {
+        if (
+          node.right.type === AST_NODE_TYPES.Literal &&
+          node.right.value === '' &&
+          doesUnderlyingTypeMatchFlag(
+            services.getTypeAtLocation(node.left),
+            ts.TypeFlags.StringLike,
+          )
+        ) {
+          const wrappingFixerParams = {
+            node,
+            innerNode: [node.left],
+            sourceCode: context.sourceCode,
+          };
+
+          context.report({
+            node,
+            messageId: 'unnecessaryTypeConversion',
+            data: {
+              type: 'string',
+              violation: "Concatenating a string with ''",
+            },
+            fix:
+              node.parent.type === AST_NODE_TYPES.ExpressionStatement
+                ? (fixer: RuleFixer): RuleFix[] => [
+                    fixer.removeRange([
+                      node.parent.range[0],
+                      node.parent.range[1],
+                    ]),
+                  ]
+                : getWrappingFixer(wrappingFixerParams),
+            suggest: [
+              {
+                messageId: 'unnecessaryTypeConversionSuggestion',
+                data: { type: 'string' },
+                fix: getWrappingFixer({
+                  ...wrappingFixerParams,
+                  wrap: expr => `${expr} satisfies string`,
+                }),
+              },
+            ],
+          });
+        }
+      },
+      'BinaryExpression[operator = "+"]'(
+        node: TSESTree.BinaryExpression,
+      ): void {
+        if (
+          node.right.type === AST_NODE_TYPES.Literal &&
+          node.right.value === '' &&
+          doesUnderlyingTypeMatchFlag(
+            services.getTypeAtLocation(node.left),
+            ts.TypeFlags.StringLike,
+          )
+        ) {
+          const wrappingFixerParams = {
+            node,
+            innerNode: [node.left],
+            sourceCode: context.sourceCode,
+          };
+
+          context.report({
+            loc: {
+              start: node.left.loc.end,
+              end: node.loc.end,
+            },
+            messageId: 'unnecessaryTypeConversion',
+            data: {
+              type: 'string',
+              violation: "Concatenating a string with ''",
+            },
+            fix: getWrappingFixer(wrappingFixerParams),
+            suggest: [
+              {
+                messageId: 'unnecessaryTypeConversionSuggestion',
+                data: { type: 'string' },
+                fix: getWrappingFixer({
+                  ...wrappingFixerParams,
+                  wrap: expr => `${expr} satisfies string`,
+                }),
+              },
+            ],
+          });
+        } else if (
+          node.left.type === AST_NODE_TYPES.Literal &&
+          node.left.value === '' &&
+          doesUnderlyingTypeMatchFlag(
+            services.getTypeAtLocation(node.right),
+            ts.TypeFlags.StringLike,
+          )
+        ) {
+          const wrappingFixerParams = {
+            node,
+            innerNode: [node.right],
+            sourceCode: context.sourceCode,
+          };
+
+          context.report({
+            loc: {
+              start: node.loc.start,
+              end: node.right.loc.start,
+            },
+            messageId: 'unnecessaryTypeConversion',
+            data: {
+              type: 'string',
+              violation: "Concatenating '' with a string",
+            },
+            fix: getWrappingFixer(wrappingFixerParams),
+            suggest: [
+              {
+                messageId: 'unnecessaryTypeConversionSuggestion',
+                data: { type: 'string' },
+                fix: getWrappingFixer({
+                  ...wrappingFixerParams,
+                  wrap: expr => `${expr} satisfies string`,
+                }),
+              },
+            ],
+          });
+        }
+      },
+      CallExpression(node: TSESTree.CallExpression): void {
+        if (
+          node.callee.type === AST_NODE_TYPES.Identifier &&
+          node.arguments.length === 1
+        ) {
+          const getType = () =>
+            getConstrainedTypeAtLocation(services, node.arguments[0]);
+
+          const isBuiltInCall = (name: string) => {
+            if ((node.callee as TSESTree.Identifier).name === name) {
path: packages/eslint-plugin/src/rules/no-unnecessary-type-conversion.ts
user: login: kirkwaiblinger
body: You can avoid this type assertion by 
1. Define a type predicate for the node type `const isIdentifier = isNodeOfType(AST_NODE_TYPES.Identifier)`
2. Alias the `node.callee` to use it in the closure, 
```ts
      CallExpression(node: TSESTree.CallExpression): void {
        if (
          isIdentifier(node.callee) &&
          node.arguments.length === 1
        ) {
          const nodeCallee = node.callee;
          const getType = () =>
            getConstrainedTypeAtLocation(services, node.arguments[0]);

          function isBuiltInCall(name: string) {
            if (nodeCallee.name === name) {
              const scope = context.sourceCode.getScope(node);
              const variable = scope.set.get(name);
              return !variable?.defs.length;
            }
            return false;
          }
```

created_at: 2025-02-10T02:51:42Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,369 @@
+import type { TSESTree } from '@typescript-eslint/utils';
+import type {
+  ReportDescriptorMessageData,
+  RuleFix,
+  RuleFixer,
+} from '@typescript-eslint/utils/ts-eslint';
+
+import { AST_NODE_TYPES } from '@typescript-eslint/utils';
+import * as ts from 'typescript';
+
+import {
+  createRule,
+  getConstrainedTypeAtLocation,
+  getParserServices,
+  getWrappingFixer,
+  isTypeFlagSet,
+} from '../util';
+
+type Options = [];
+type MessageIds =
+  | 'unnecessaryTypeConversion'
+  | 'unnecessaryTypeConversionSuggestion';
+
+export default createRule<Options, MessageIds>({
+  name: 'no-unnecessary-type-conversion',
+  meta: {
+    type: 'suggestion',
+    docs: {
+      description:
+        'Disallow conversion idioms when they do not change the type or value of the expression',
+      requiresTypeChecking: true,
+    },
+    fixable: 'code',
+    hasSuggestions: true,
+    messages: {
+      unnecessaryTypeConversion:
+        '{{violation}} does not change the type or value of the {{type}}.',
+      unnecessaryTypeConversionSuggestion:
+        'Instead, assert that the value satisfies type {{type}}.',
+    },
+    schema: [],
+  },
+  defaultOptions: [],
+  create(context) {
+    function doesUnderlyingTypeMatchFlag(
+      type: ts.Type,
+      typeFlag: ts.TypeFlags,
+    ): boolean {
+      const matchesType = (t: ts.Type): boolean => {
+        return isTypeFlagSet(t, typeFlag);
+      };
+
+      if (type.isUnion()) {
+        return type.types.every(matchesType);
+      }
+
+      return matchesType(type);
+    }
+
+    const services = getParserServices(context);
+
+    function handleUnaryOperator(
+      node: TSESTree.UnaryExpression,
+      typeFlag: ts.TypeFlags,
+      reportDescriptorMessageData: ReportDescriptorMessageData,
+      isDoubleOperator: boolean, // !! or ~~
+    ) {
+      const type = services.getTypeAtLocation(node.argument);
+      if (doesUnderlyingTypeMatchFlag(type, typeFlag)) {
+        const wrappingFixerParams = {
+          node: isDoubleOperator ? node.parent : node,
+          innerNode: [node.argument],
+          sourceCode: context.sourceCode,
+        };
+        const typeString =
+          typeFlag === ts.TypeFlags.BooleanLike ? 'boolean' : 'number';
+
+        context.report({
+          loc: {
+            start: isDoubleOperator ? node.parent.loc.start : node.loc.start,
+            end: {
+              column: node.loc.start.column + 1,
+              line: node.loc.start.line,
+            },
+          },
+          messageId: 'unnecessaryTypeConversion',
+          data: reportDescriptorMessageData,
+          fix: getWrappingFixer(wrappingFixerParams),
+          suggest: [
+            {
+              messageId: 'unnecessaryTypeConversionSuggestion',
+              data: { type: reportDescriptorMessageData.type },
+              fix: getWrappingFixer({
+                ...wrappingFixerParams,
+                wrap: expr => `${expr} satisfies ${typeString}`,
+              }),
+            },
+          ],
+        });
+      }
+    }
+
+    return {
+      'AssignmentExpression[operator = "+="]'(
+        node: TSESTree.AssignmentExpression,
+      ): void {
+        if (
+          node.right.type === AST_NODE_TYPES.Literal &&
+          node.right.value === '' &&
+          doesUnderlyingTypeMatchFlag(
+            services.getTypeAtLocation(node.left),
+            ts.TypeFlags.StringLike,
+          )
+        ) {
+          const wrappingFixerParams = {
+            node,
+            innerNode: [node.left],
+            sourceCode: context.sourceCode,
+          };
+
+          context.report({
+            node,
+            messageId: 'unnecessaryTypeConversion',
+            data: {
+              type: 'string',
+              violation: "Concatenating a string with ''",
+            },
+            fix:
+              node.parent.type === AST_NODE_TYPES.ExpressionStatement
+                ? (fixer: RuleFixer): RuleFix[] => [
+                    fixer.removeRange([
+                      node.parent.range[0],
+                      node.parent.range[1],
+                    ]),
+                  ]
+                : getWrappingFixer(wrappingFixerParams),
+            suggest: [
+              {
+                messageId: 'unnecessaryTypeConversionSuggestion',
+                data: { type: 'string' },
+                fix: getWrappingFixer({
+                  ...wrappingFixerParams,
+                  wrap: expr => `${expr} satisfies string`,
+                }),
+              },
+            ],
+          });
+        }
+      },
+      'BinaryExpression[operator = "+"]'(
+        node: TSESTree.BinaryExpression,
+      ): void {
+        if (
+          node.right.type === AST_NODE_TYPES.Literal &&
+          node.right.value === '' &&
+          doesUnderlyingTypeMatchFlag(
+            services.getTypeAtLocation(node.left),
+            ts.TypeFlags.StringLike,
+          )
+        ) {
+          const wrappingFixerParams = {
+            node,
+            innerNode: [node.left],
+            sourceCode: context.sourceCode,
+          };
+
+          context.report({
+            loc: {
+              start: node.left.loc.end,
+              end: node.loc.end,
+            },
+            messageId: 'unnecessaryTypeConversion',
+            data: {
+              type: 'string',
+              violation: "Concatenating a string with ''",
+            },
+            fix: getWrappingFixer(wrappingFixerParams),
+            suggest: [
+              {
+                messageId: 'unnecessaryTypeConversionSuggestion',
+                data: { type: 'string' },
+                fix: getWrappingFixer({
+                  ...wrappingFixerParams,
+                  wrap: expr => `${expr} satisfies string`,
+                }),
+              },
+            ],
+          });
+        } else if (
+          node.left.type === AST_NODE_TYPES.Literal &&
+          node.left.value === '' &&
+          doesUnderlyingTypeMatchFlag(
+            services.getTypeAtLocation(node.right),
+            ts.TypeFlags.StringLike,
+          )
+        ) {
+          const wrappingFixerParams = {
+            node,
+            innerNode: [node.right],
+            sourceCode: context.sourceCode,
+          };
+
+          context.report({
+            loc: {
+              start: node.loc.start,
+              end: node.right.loc.start,
+            },
+            messageId: 'unnecessaryTypeConversion',
+            data: {
+              type: 'string',
+              violation: "Concatenating '' with a string",
+            },
+            fix: getWrappingFixer(wrappingFixerParams),
+            suggest: [
+              {
+                messageId: 'unnecessaryTypeConversionSuggestion',
+                data: { type: 'string' },
+                fix: getWrappingFixer({
+                  ...wrappingFixerParams,
+                  wrap: expr => `${expr} satisfies string`,
+                }),
+              },
+            ],
+          });
+        }
+      },
+      CallExpression(node: TSESTree.CallExpression): void {
+        if (
+          node.callee.type === AST_NODE_TYPES.Identifier &&
+          node.arguments.length === 1
+        ) {
+          const getType = () =>
+            getConstrainedTypeAtLocation(services, node.arguments[0]);
+
+          const isBuiltInCall = (name: string) => {
+            if ((node.callee as TSESTree.Identifier).name === name) {
+              const scope = context.sourceCode.getScope(node);
+              const variable = scope.set.get(name);
+              return !variable?.defs.length;
+            }
+            return false;
+          };
+
+          if (
+            // eslint-disable-next-line @typescript-eslint/internal/prefer-ast-types-enum
+            (isBuiltInCall('String') &&
+              doesUnderlyingTypeMatchFlag(
+                getType(),
+                ts.TypeFlags.StringLike,
+              )) ||
+            (isBuiltInCall('Number') &&
+              doesUnderlyingTypeMatchFlag(
+                getType(),
+                ts.TypeFlags.NumberLike,
+              )) ||
+            // eslint-disable-next-line @typescript-eslint/internal/prefer-ast-types-enum
+            (isBuiltInCall('Boolean') &&
+              doesUnderlyingTypeMatchFlag(
+                getType(),
+                ts.TypeFlags.BooleanLike,
+              )) ||
+            (isBuiltInCall('BigInt') &&
+              doesUnderlyingTypeMatchFlag(getType(), ts.TypeFlags.BigIntLike))
+          ) {
+            const wrappingFixerParams = {
+              node,
+              innerNode: [node.arguments[0]],
+              sourceCode: context.sourceCode,
+            };
+            const typeString = node.callee.name.toLowerCase();
+
+            context.report({
+              loc: node.callee.loc,
path: packages/eslint-plugin/src/rules/no-unnecessary-type-conversion.ts
user: login: kirkwaiblinger
body: Can just do this as 
```suggestion
              node: node.callee,
```
created_at: 2025-02-10T02:54:29Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,369 @@
+import type { TSESTree } from '@typescript-eslint/utils';
+import type {
+  ReportDescriptorMessageData,
+  RuleFix,
+  RuleFixer,
+} from '@typescript-eslint/utils/ts-eslint';
+
+import { AST_NODE_TYPES } from '@typescript-eslint/utils';
+import * as ts from 'typescript';
+
+import {
+  createRule,
+  getConstrainedTypeAtLocation,
+  getParserServices,
+  getWrappingFixer,
+  isTypeFlagSet,
+} from '../util';
+
+type Options = [];
+type MessageIds =
+  | 'unnecessaryTypeConversion'
+  | 'unnecessaryTypeConversionSuggestion';
+
+export default createRule<Options, MessageIds>({
+  name: 'no-unnecessary-type-conversion',
+  meta: {
+    type: 'suggestion',
+    docs: {
+      description:
+        'Disallow conversion idioms when they do not change the type or value of the expression',
+      requiresTypeChecking: true,
+    },
+    fixable: 'code',
+    hasSuggestions: true,
+    messages: {
+      unnecessaryTypeConversion:
+        '{{violation}} does not change the type or value of the {{type}}.',
+      unnecessaryTypeConversionSuggestion:
+        'Instead, assert that the value satisfies type {{type}}.',
+    },
+    schema: [],
+  },
+  defaultOptions: [],
+  create(context) {
+    function doesUnderlyingTypeMatchFlag(
+      type: ts.Type,
+      typeFlag: ts.TypeFlags,
+    ): boolean {
+      const matchesType = (t: ts.Type): boolean => {
+        return isTypeFlagSet(t, typeFlag);
+      };
+
+      if (type.isUnion()) {
+        return type.types.every(matchesType);
+      }
+
+      return matchesType(type);
+    }
+
+    const services = getParserServices(context);
+
+    function handleUnaryOperator(
+      node: TSESTree.UnaryExpression,
+      typeFlag: ts.TypeFlags,
+      reportDescriptorMessageData: ReportDescriptorMessageData,
+      isDoubleOperator: boolean, // !! or ~~
+    ) {
+      const type = services.getTypeAtLocation(node.argument);
+      if (doesUnderlyingTypeMatchFlag(type, typeFlag)) {
+        const wrappingFixerParams = {
+          node: isDoubleOperator ? node.parent : node,
+          innerNode: [node.argument],
+          sourceCode: context.sourceCode,
+        };
+        const typeString =
+          typeFlag === ts.TypeFlags.BooleanLike ? 'boolean' : 'number';
+
+        context.report({
+          loc: {
+            start: isDoubleOperator ? node.parent.loc.start : node.loc.start,
+            end: {
+              column: node.loc.start.column + 1,
+              line: node.loc.start.line,
+            },
+          },
+          messageId: 'unnecessaryTypeConversion',
+          data: reportDescriptorMessageData,
+          fix: getWrappingFixer(wrappingFixerParams),
+          suggest: [
+            {
+              messageId: 'unnecessaryTypeConversionSuggestion',
+              data: { type: reportDescriptorMessageData.type },
+              fix: getWrappingFixer({
+                ...wrappingFixerParams,
+                wrap: expr => `${expr} satisfies ${typeString}`,
+              }),
+            },
+          ],
+        });
+      }
+    }
+
+    return {
+      'AssignmentExpression[operator = "+="]'(
+        node: TSESTree.AssignmentExpression,
+      ): void {
+        if (
+          node.right.type === AST_NODE_TYPES.Literal &&
+          node.right.value === '' &&
+          doesUnderlyingTypeMatchFlag(
+            services.getTypeAtLocation(node.left),
+            ts.TypeFlags.StringLike,
+          )
+        ) {
+          const wrappingFixerParams = {
+            node,
+            innerNode: [node.left],
+            sourceCode: context.sourceCode,
+          };
+
+          context.report({
+            node,
+            messageId: 'unnecessaryTypeConversion',
+            data: {
+              type: 'string',
+              violation: "Concatenating a string with ''",
+            },
+            fix:
+              node.parent.type === AST_NODE_TYPES.ExpressionStatement
+                ? (fixer: RuleFixer): RuleFix[] => [
+                    fixer.removeRange([
+                      node.parent.range[0],
+                      node.parent.range[1],
+                    ]),
+                  ]
+                : getWrappingFixer(wrappingFixerParams),
+            suggest: [
+              {
+                messageId: 'unnecessaryTypeConversionSuggestion',
+                data: { type: 'string' },
+                fix: getWrappingFixer({
+                  ...wrappingFixerParams,
+                  wrap: expr => `${expr} satisfies string`,
+                }),
+              },
+            ],
+          });
+        }
+      },
+      'BinaryExpression[operator = "+"]'(
+        node: TSESTree.BinaryExpression,
+      ): void {
+        if (
+          node.right.type === AST_NODE_TYPES.Literal &&
+          node.right.value === '' &&
+          doesUnderlyingTypeMatchFlag(
+            services.getTypeAtLocation(node.left),
+            ts.TypeFlags.StringLike,
+          )
+        ) {
+          const wrappingFixerParams = {
+            node,
+            innerNode: [node.left],
+            sourceCode: context.sourceCode,
+          };
+
+          context.report({
+            loc: {
+              start: node.left.loc.end,
+              end: node.loc.end,
+            },
+            messageId: 'unnecessaryTypeConversion',
+            data: {
+              type: 'string',
+              violation: "Concatenating a string with ''",
+            },
+            fix: getWrappingFixer(wrappingFixerParams),
+            suggest: [
+              {
+                messageId: 'unnecessaryTypeConversionSuggestion',
+                data: { type: 'string' },
+                fix: getWrappingFixer({
+                  ...wrappingFixerParams,
+                  wrap: expr => `${expr} satisfies string`,
+                }),
+              },
+            ],
+          });
+        } else if (
+          node.left.type === AST_NODE_TYPES.Literal &&
+          node.left.value === '' &&
+          doesUnderlyingTypeMatchFlag(
+            services.getTypeAtLocation(node.right),
+            ts.TypeFlags.StringLike,
+          )
+        ) {
+          const wrappingFixerParams = {
+            node,
+            innerNode: [node.right],
+            sourceCode: context.sourceCode,
+          };
+
+          context.report({
+            loc: {
+              start: node.loc.start,
+              end: node.right.loc.start,
+            },
+            messageId: 'unnecessaryTypeConversion',
+            data: {
+              type: 'string',
+              violation: "Concatenating '' with a string",
+            },
+            fix: getWrappingFixer(wrappingFixerParams),
+            suggest: [
+              {
+                messageId: 'unnecessaryTypeConversionSuggestion',
+                data: { type: 'string' },
+                fix: getWrappingFixer({
+                  ...wrappingFixerParams,
+                  wrap: expr => `${expr} satisfies string`,
+                }),
+              },
+            ],
+          });
+        }
+      },
+      CallExpression(node: TSESTree.CallExpression): void {
+        if (
+          node.callee.type === AST_NODE_TYPES.Identifier &&
+          node.arguments.length === 1
+        ) {
+          const getType = () =>
+            getConstrainedTypeAtLocation(services, node.arguments[0]);
+
+          const isBuiltInCall = (name: string) => {
+            if ((node.callee as TSESTree.Identifier).name === name) {
+              const scope = context.sourceCode.getScope(node);
+              const variable = scope.set.get(name);
+              return !variable?.defs.length;
+            }
+            return false;
+          };
+
+          if (
+            // eslint-disable-next-line @typescript-eslint/internal/prefer-ast-types-enum
+            (isBuiltInCall('String') &&
+              doesUnderlyingTypeMatchFlag(
+                getType(),
+                ts.TypeFlags.StringLike,
+              )) ||
+            (isBuiltInCall('Number') &&
+              doesUnderlyingTypeMatchFlag(
+                getType(),
+                ts.TypeFlags.NumberLike,
+              )) ||
+            // eslint-disable-next-line @typescript-eslint/internal/prefer-ast-types-enum
+            (isBuiltInCall('Boolean') &&
+              doesUnderlyingTypeMatchFlag(
+                getType(),
+                ts.TypeFlags.BooleanLike,
+              )) ||
+            (isBuiltInCall('BigInt') &&
+              doesUnderlyingTypeMatchFlag(getType(), ts.TypeFlags.BigIntLike))
+          ) {
+            const wrappingFixerParams = {
+              node,
+              innerNode: [node.arguments[0]],
+              sourceCode: context.sourceCode,
+            };
+            const typeString = node.callee.name.toLowerCase();
+
+            context.report({
+              loc: node.callee.loc,
+              messageId: 'unnecessaryTypeConversion',
+              data: {
+                type: node.callee.name.toLowerCase(),
+                violation: `Passing a ${typeString} to ${node.callee.name}()`,
+              },
+              fix: getWrappingFixer(wrappingFixerParams),
+              suggest: [
+                {
+                  messageId: 'unnecessaryTypeConversionSuggestion',
+                  data: { type: typeString },
+                  fix: getWrappingFixer({
+                    ...wrappingFixerParams,
+                    wrap: expr => `${expr} satisfies ${typeString}`,
+                  }),
+                },
+              ],
+            });
+          }
+        }
+      },
+      'CallExpression > MemberExpression.callee > Identifier[name = "toString"].property'(
path: packages/eslint-plugin/src/rules/no-unnecessary-type-conversion.ts
user: login: kirkwaiblinger
body: Today I learned that you could use ESQuery with the `.callee` like this. Cool!

Question, though, does the `.property` at the end do anything? I can't think what it would do and no tests fail if I change it to 

```suggestion
      'CallExpression > MemberExpression.callee > Identifier[name = "toString"]'(
```
created_at: 2025-02-10T03:06:03Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,369 @@
+import type { TSESTree } from '@typescript-eslint/utils';
+import type {
+  ReportDescriptorMessageData,
+  RuleFix,
+  RuleFixer,
+} from '@typescript-eslint/utils/ts-eslint';
+
+import { AST_NODE_TYPES } from '@typescript-eslint/utils';
+import * as ts from 'typescript';
+
+import {
+  createRule,
+  getConstrainedTypeAtLocation,
+  getParserServices,
+  getWrappingFixer,
+  isTypeFlagSet,
+} from '../util';
+
+type Options = [];
+type MessageIds =
+  | 'unnecessaryTypeConversion'
+  | 'unnecessaryTypeConversionSuggestion';
+
+export default createRule<Options, MessageIds>({
+  name: 'no-unnecessary-type-conversion',
+  meta: {
+    type: 'suggestion',
+    docs: {
+      description:
+        'Disallow conversion idioms when they do not change the type or value of the expression',
+      requiresTypeChecking: true,
+    },
+    fixable: 'code',
+    hasSuggestions: true,
+    messages: {
+      unnecessaryTypeConversion:
+        '{{violation}} does not change the type or value of the {{type}}.',
+      unnecessaryTypeConversionSuggestion:
+        'Instead, assert that the value satisfies type {{type}}.',
+    },
+    schema: [],
+  },
+  defaultOptions: [],
+  create(context) {
+    function doesUnderlyingTypeMatchFlag(
+      type: ts.Type,
+      typeFlag: ts.TypeFlags,
+    ): boolean {
+      const matchesType = (t: ts.Type): boolean => {
+        return isTypeFlagSet(t, typeFlag);
+      };
+
+      if (type.isUnion()) {
+        return type.types.every(matchesType);
+      }
+
+      return matchesType(type);
+    }
+
+    const services = getParserServices(context);
+
+    function handleUnaryOperator(
+      node: TSESTree.UnaryExpression,
+      typeFlag: ts.TypeFlags,
+      reportDescriptorMessageData: ReportDescriptorMessageData,
+      isDoubleOperator: boolean, // !! or ~~
+    ) {
+      const type = services.getTypeAtLocation(node.argument);
+      if (doesUnderlyingTypeMatchFlag(type, typeFlag)) {
+        const wrappingFixerParams = {
+          node: isDoubleOperator ? node.parent : node,
+          innerNode: [node.argument],
+          sourceCode: context.sourceCode,
+        };
+        const typeString =
+          typeFlag === ts.TypeFlags.BooleanLike ? 'boolean' : 'number';
+
+        context.report({
+          loc: {
+            start: isDoubleOperator ? node.parent.loc.start : node.loc.start,
+            end: {
+              column: node.loc.start.column + 1,
+              line: node.loc.start.line,
+            },
+          },
+          messageId: 'unnecessaryTypeConversion',
+          data: reportDescriptorMessageData,
+          fix: getWrappingFixer(wrappingFixerParams),
+          suggest: [
+            {
+              messageId: 'unnecessaryTypeConversionSuggestion',
+              data: { type: reportDescriptorMessageData.type },
+              fix: getWrappingFixer({
+                ...wrappingFixerParams,
+                wrap: expr => `${expr} satisfies ${typeString}`,
+              }),
+            },
+          ],
+        });
+      }
+    }
+
+    return {
+      'AssignmentExpression[operator = "+="]'(
+        node: TSESTree.AssignmentExpression,
+      ): void {
+        if (
+          node.right.type === AST_NODE_TYPES.Literal &&
+          node.right.value === '' &&
+          doesUnderlyingTypeMatchFlag(
+            services.getTypeAtLocation(node.left),
+            ts.TypeFlags.StringLike,
+          )
+        ) {
+          const wrappingFixerParams = {
+            node,
+            innerNode: [node.left],
+            sourceCode: context.sourceCode,
+          };
+
+          context.report({
+            node,
+            messageId: 'unnecessaryTypeConversion',
+            data: {
+              type: 'string',
+              violation: "Concatenating a string with ''",
+            },
+            fix:
+              node.parent.type === AST_NODE_TYPES.ExpressionStatement
+                ? (fixer: RuleFixer): RuleFix[] => [
+                    fixer.removeRange([
+                      node.parent.range[0],
+                      node.parent.range[1],
+                    ]),
+                  ]
+                : getWrappingFixer(wrappingFixerParams),
+            suggest: [
+              {
+                messageId: 'unnecessaryTypeConversionSuggestion',
+                data: { type: 'string' },
+                fix: getWrappingFixer({
+                  ...wrappingFixerParams,
+                  wrap: expr => `${expr} satisfies string`,
+                }),
+              },
+            ],
+          });
+        }
+      },
+      'BinaryExpression[operator = "+"]'(
+        node: TSESTree.BinaryExpression,
+      ): void {
+        if (
+          node.right.type === AST_NODE_TYPES.Literal &&
+          node.right.value === '' &&
+          doesUnderlyingTypeMatchFlag(
+            services.getTypeAtLocation(node.left),
+            ts.TypeFlags.StringLike,
+          )
+        ) {
+          const wrappingFixerParams = {
+            node,
+            innerNode: [node.left],
+            sourceCode: context.sourceCode,
+          };
+
+          context.report({
+            loc: {
+              start: node.left.loc.end,
+              end: node.loc.end,
+            },
+            messageId: 'unnecessaryTypeConversion',
+            data: {
+              type: 'string',
+              violation: "Concatenating a string with ''",
+            },
+            fix: getWrappingFixer(wrappingFixerParams),
+            suggest: [
+              {
+                messageId: 'unnecessaryTypeConversionSuggestion',
+                data: { type: 'string' },
+                fix: getWrappingFixer({
+                  ...wrappingFixerParams,
+                  wrap: expr => `${expr} satisfies string`,
+                }),
+              },
+            ],
+          });
+        } else if (
+          node.left.type === AST_NODE_TYPES.Literal &&
+          node.left.value === '' &&
+          doesUnderlyingTypeMatchFlag(
+            services.getTypeAtLocation(node.right),
+            ts.TypeFlags.StringLike,
+          )
+        ) {
+          const wrappingFixerParams = {
+            node,
+            innerNode: [node.right],
+            sourceCode: context.sourceCode,
+          };
+
+          context.report({
+            loc: {
+              start: node.loc.start,
+              end: node.right.loc.start,
+            },
+            messageId: 'unnecessaryTypeConversion',
+            data: {
+              type: 'string',
+              violation: "Concatenating '' with a string",
+            },
+            fix: getWrappingFixer(wrappingFixerParams),
+            suggest: [
+              {
+                messageId: 'unnecessaryTypeConversionSuggestion',
+                data: { type: 'string' },
+                fix: getWrappingFixer({
+                  ...wrappingFixerParams,
+                  wrap: expr => `${expr} satisfies string`,
+                }),
+              },
+            ],
+          });
+        }
+      },
+      CallExpression(node: TSESTree.CallExpression): void {
+        if (
+          node.callee.type === AST_NODE_TYPES.Identifier &&
+          node.arguments.length === 1
+        ) {
+          const getType = () =>
+            getConstrainedTypeAtLocation(services, node.arguments[0]);
+
+          const isBuiltInCall = (name: string) => {
+            if ((node.callee as TSESTree.Identifier).name === name) {
+              const scope = context.sourceCode.getScope(node);
+              const variable = scope.set.get(name);
+              return !variable?.defs.length;
+            }
+            return false;
+          };
+
+          if (
+            // eslint-disable-next-line @typescript-eslint/internal/prefer-ast-types-enum
+            (isBuiltInCall('String') &&
+              doesUnderlyingTypeMatchFlag(
+                getType(),
+                ts.TypeFlags.StringLike,
+              )) ||
+            (isBuiltInCall('Number') &&
+              doesUnderlyingTypeMatchFlag(
+                getType(),
+                ts.TypeFlags.NumberLike,
+              )) ||
+            // eslint-disable-next-line @typescript-eslint/internal/prefer-ast-types-enum
+            (isBuiltInCall('Boolean') &&
+              doesUnderlyingTypeMatchFlag(
+                getType(),
+                ts.TypeFlags.BooleanLike,
+              )) ||
+            (isBuiltInCall('BigInt') &&
+              doesUnderlyingTypeMatchFlag(getType(), ts.TypeFlags.BigIntLike))
+          ) {
+            const wrappingFixerParams = {
+              node,
+              innerNode: [node.arguments[0]],
+              sourceCode: context.sourceCode,
+            };
+            const typeString = node.callee.name.toLowerCase();
+
+            context.report({
+              loc: node.callee.loc,
+              messageId: 'unnecessaryTypeConversion',
+              data: {
+                type: node.callee.name.toLowerCase(),
+                violation: `Passing a ${typeString} to ${node.callee.name}()`,
+              },
+              fix: getWrappingFixer(wrappingFixerParams),
+              suggest: [
+                {
+                  messageId: 'unnecessaryTypeConversionSuggestion',
+                  data: { type: typeString },
+                  fix: getWrappingFixer({
+                    ...wrappingFixerParams,
+                    wrap: expr => `${expr} satisfies ${typeString}`,
+                  }),
+                },
+              ],
+            });
+          }
+        }
+      },
+      'CallExpression > MemberExpression.callee > Identifier[name = "toString"].property'(
path: packages/eslint-plugin/src/rules/no-unnecessary-type-conversion.ts
user: login: kirkwaiblinger
body: To make sure the selector is correct could you also add the following (valid) test case?
```ts
export {};
declare const toString: string;
toString.toUpperCase();
```
created_at: 2025-02-10T03:12:35Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,461 @@
+import type { TSESTree } from '@typescript-eslint/utils';
+import type {
+  ReportDescriptorMessageData,
+  RuleFix,
+  RuleFixer,
+} from '@typescript-eslint/utils/ts-eslint';
+
+import { AST_NODE_TYPES } from '@typescript-eslint/utils';
+import * as ts from 'typescript';
+
+import {
+  createRule,
+  getConstrainedTypeAtLocation,
+  getParserServices,
+  isTypeFlagSet,
+} from '../util';
+
+type Options = [];
+type MessageIds =
+  | 'unnecessaryTypeConversion'
+  | 'unnecessaryTypeConversionSuggestion';
+
+export default createRule<Options, MessageIds>({
+  name: 'no-unnecessary-type-conversion',
+  meta: {
+    type: 'suggestion',
+    docs: {
+      description:
+        'Disallow conversion idioms when they do not change the type or value of the expression',
+      requiresTypeChecking: true,
+    },
+    fixable: 'code',
+    hasSuggestions: true,
+    messages: {
+      unnecessaryTypeConversion:
+        '{{violation}} does not change the type or value of the {{type}}.',
+      unnecessaryTypeConversionSuggestion:
+        'Instead, assert that the value satisfies type {{type}}.',
+    },
+    schema: [],
+  },
+  defaultOptions: [],
+  create(context) {
+    function doesUnderlyingTypeMatchFlag(
+      type: ts.Type,
+      typeFlag: ts.TypeFlags,
+    ): boolean {
+      const matchesType = (t: ts.Type): boolean => {
+        return isTypeFlagSet(t, typeFlag);
+      };
+
+      if (type.isUnion()) {
+        return type.types.every(matchesType);
+      }
+
+      if (type.isIntersection()) {
+        return type.types.some(matchesType);
+      }
+
+      return matchesType(type);
+    }
+
+    const doesTypeRequireParentheses = (type: AST_NODE_TYPES) =>
+      ![
+        AST_NODE_TYPES.Literal,
+        AST_NODE_TYPES.Identifier,
+        AST_NODE_TYPES.UnaryExpression,
+        AST_NODE_TYPES.CallExpression,
+        AST_NODE_TYPES.MemberExpression,
+      ].includes(type);
+
+    const shouldHaveSatisfiesSuggestion = (type: AST_NODE_TYPES) =>
+      [AST_NODE_TYPES.Identifier, AST_NODE_TYPES.MemberExpression].includes(
+        type,
+      );
+
+    const services = getParserServices(context);
+    const checker = services.program.getTypeChecker();
+
+    const surroundWithParentheses = (
path: packages/eslint-plugin/src/rules/no-unnecessary-type-conversion.ts
user: login: kirkwaiblinger
body: Would you mind providing a few examples? Would save me quite a bit of time 🙏 In general, we're not too stressed about some extra parens, which most users are likely to fix with a formatter anyway, unlike too few parens, which will actually break people's code.
created_at: 2025-02-10T03:25:39Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,467 @@
+import { RuleTester } from '@typescript-eslint/rule-tester';
+
+import rule from '../../src/rules/no-unnecessary-type-conversion';
+import { getFixturesRootDir } from '../RuleTester';
+
+const rootDir = getFixturesRootDir();
+
+const ruleTester = new RuleTester({
+  languageOptions: {
+    parserOptions: {
+      project: './tsconfig.json',
+      tsconfigRootDir: rootDir,
+    },
+  },
+});
+
+ruleTester.run('no-unnecessary-type-conversion', rule, {
+  valid: [
path: packages/eslint-plugin/tests/rules/no-unnecessary-type-conversion.test.ts
user: login: kirkwaiblinger
body: (testing)
Could you please add some valid cases around ensuring that only _primitives_ cause the rule to flag? In particular, I'm thinking about the boxed primitives, so, things like
```ts
String(new String('foo'));

function f(x: Number) {
    return +x;
}
```
etc

Also it would be good to add some tests around stringy and numbery `enum`s. 

Also let's add some coverage around generics, i.e.
```ts
function f<T extends string>(x: T) {
    return String(x);
}
```
created_at: 2025-02-10T03:43:02Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,369 @@
+import type { TSESTree } from '@typescript-eslint/utils';
+import type {
+  ReportDescriptorMessageData,
+  RuleFix,
+  RuleFixer,
+} from '@typescript-eslint/utils/ts-eslint';
+
+import { AST_NODE_TYPES } from '@typescript-eslint/utils';
+import * as ts from 'typescript';
+
+import {
+  createRule,
+  getConstrainedTypeAtLocation,
+  getParserServices,
+  getWrappingFixer,
+  isTypeFlagSet,
+} from '../util';
+
+type Options = [];
+type MessageIds =
+  | 'unnecessaryTypeConversion'
+  | 'unnecessaryTypeConversionSuggestion';
+
+export default createRule<Options, MessageIds>({
+  name: 'no-unnecessary-type-conversion',
+  meta: {
+    type: 'suggestion',
+    docs: {
+      description:
+        'Disallow conversion idioms when they do not change the type or value of the expression',
+      requiresTypeChecking: true,
+    },
+    fixable: 'code',
+    hasSuggestions: true,
+    messages: {
+      unnecessaryTypeConversion:
+        '{{violation}} does not change the type or value of the {{type}}.',
+      unnecessaryTypeConversionSuggestion:
+        'Instead, assert that the value satisfies type {{type}}.',
+    },
+    schema: [],
+  },
+  defaultOptions: [],
+  create(context) {
+    function doesUnderlyingTypeMatchFlag(
+      type: ts.Type,
+      typeFlag: ts.TypeFlags,
+    ): boolean {
+      const matchesType = (t: ts.Type): boolean => {
+        return isTypeFlagSet(t, typeFlag);
+      };
+
+      if (type.isUnion()) {
+        return type.types.every(matchesType);
+      }
+
+      return matchesType(type);
+    }
+
+    const services = getParserServices(context);
+
+    function handleUnaryOperator(
+      node: TSESTree.UnaryExpression,
+      typeFlag: ts.TypeFlags,
+      reportDescriptorMessageData: ReportDescriptorMessageData,
+      isDoubleOperator: boolean, // !! or ~~
+    ) {
+      const type = services.getTypeAtLocation(node.argument);
+      if (doesUnderlyingTypeMatchFlag(type, typeFlag)) {
+        const wrappingFixerParams = {
+          node: isDoubleOperator ? node.parent : node,
+          innerNode: [node.argument],
+          sourceCode: context.sourceCode,
+        };
+        const typeString =
+          typeFlag === ts.TypeFlags.BooleanLike ? 'boolean' : 'number';
+
+        context.report({
+          loc: {
+            start: isDoubleOperator ? node.parent.loc.start : node.loc.start,
+            end: {
+              column: node.loc.start.column + 1,
+              line: node.loc.start.line,
+            },
+          },
+          messageId: 'unnecessaryTypeConversion',
+          data: reportDescriptorMessageData,
+          fix: getWrappingFixer(wrappingFixerParams),
+          suggest: [
+            {
+              messageId: 'unnecessaryTypeConversionSuggestion',
+              data: { type: reportDescriptorMessageData.type },
+              fix: getWrappingFixer({
+                ...wrappingFixerParams,
+                wrap: expr => `${expr} satisfies ${typeString}`,
+              }),
+            },
+          ],
+        });
+      }
+    }
+
+    return {
+      'AssignmentExpression[operator = "+="]'(
+        node: TSESTree.AssignmentExpression,
+      ): void {
+        if (
+          node.right.type === AST_NODE_TYPES.Literal &&
+          node.right.value === '' &&
+          doesUnderlyingTypeMatchFlag(
+            services.getTypeAtLocation(node.left),
+            ts.TypeFlags.StringLike,
+          )
+        ) {
+          const wrappingFixerParams = {
+            node,
+            innerNode: [node.left],
+            sourceCode: context.sourceCode,
+          };
+
+          context.report({
+            node,
+            messageId: 'unnecessaryTypeConversion',
+            data: {
+              type: 'string',
+              violation: "Concatenating a string with ''",
+            },
+            fix:
+              node.parent.type === AST_NODE_TYPES.ExpressionStatement
+                ? (fixer: RuleFixer): RuleFix[] => [
+                    fixer.removeRange([
+                      node.parent.range[0],
+                      node.parent.range[1],
+                    ]),
+                  ]
+                : getWrappingFixer(wrappingFixerParams),
+            suggest: [
+              {
+                messageId: 'unnecessaryTypeConversionSuggestion',
+                data: { type: 'string' },
+                fix: getWrappingFixer({
+                  ...wrappingFixerParams,
+                  wrap: expr => `${expr} satisfies string`,
+                }),
+              },
+            ],
+          });
+        }
+      },
+      'BinaryExpression[operator = "+"]'(
+        node: TSESTree.BinaryExpression,
+      ): void {
+        if (
+          node.right.type === AST_NODE_TYPES.Literal &&
+          node.right.value === '' &&
+          doesUnderlyingTypeMatchFlag(
+            services.getTypeAtLocation(node.left),
+            ts.TypeFlags.StringLike,
+          )
+        ) {
+          const wrappingFixerParams = {
+            node,
+            innerNode: [node.left],
+            sourceCode: context.sourceCode,
+          };
+
+          context.report({
+            loc: {
+              start: node.left.loc.end,
+              end: node.loc.end,
+            },
+            messageId: 'unnecessaryTypeConversion',
+            data: {
+              type: 'string',
+              violation: "Concatenating a string with ''",
+            },
+            fix: getWrappingFixer(wrappingFixerParams),
+            suggest: [
+              {
+                messageId: 'unnecessaryTypeConversionSuggestion',
+                data: { type: 'string' },
+                fix: getWrappingFixer({
+                  ...wrappingFixerParams,
+                  wrap: expr => `${expr} satisfies string`,
+                }),
+              },
+            ],
+          });
+        } else if (
+          node.left.type === AST_NODE_TYPES.Literal &&
+          node.left.value === '' &&
+          doesUnderlyingTypeMatchFlag(
+            services.getTypeAtLocation(node.right),
+            ts.TypeFlags.StringLike,
+          )
+        ) {
+          const wrappingFixerParams = {
+            node,
+            innerNode: [node.right],
+            sourceCode: context.sourceCode,
+          };
+
+          context.report({
+            loc: {
+              start: node.loc.start,
+              end: node.right.loc.start,
+            },
+            messageId: 'unnecessaryTypeConversion',
+            data: {
+              type: 'string',
+              violation: "Concatenating '' with a string",
+            },
+            fix: getWrappingFixer(wrappingFixerParams),
+            suggest: [
+              {
+                messageId: 'unnecessaryTypeConversionSuggestion',
+                data: { type: 'string' },
+                fix: getWrappingFixer({
+                  ...wrappingFixerParams,
+                  wrap: expr => `${expr} satisfies string`,
+                }),
+              },
+            ],
+          });
+        }
+      },
+      CallExpression(node: TSESTree.CallExpression): void {
+        if (
+          node.callee.type === AST_NODE_TYPES.Identifier &&
+          node.arguments.length === 1
+        ) {
+          const getType = () =>
+            getConstrainedTypeAtLocation(services, node.arguments[0]);
+
+          const isBuiltInCall = (name: string) => {
+            if ((node.callee as TSESTree.Identifier).name === name) {
path: packages/eslint-plugin/src/rules/no-unnecessary-type-conversion.ts
user: login: kirkwaiblinger
body: Fun fact, I think it's a bug in TS that you need to alias `nodeCallee` to preserve the narrowing in the closure, see https://github.com/microsoft/TypeScript/issues/61158
created_at: 2025-02-10T08:12:21Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,369 @@
+import type { TSESTree } from '@typescript-eslint/utils';
+import type {
+  ReportDescriptorMessageData,
+  RuleFix,
+  RuleFixer,
+} from '@typescript-eslint/utils/ts-eslint';
+
+import { AST_NODE_TYPES } from '@typescript-eslint/utils';
+import * as ts from 'typescript';
+
+import {
+  createRule,
+  getConstrainedTypeAtLocation,
+  getParserServices,
+  getWrappingFixer,
+  isTypeFlagSet,
+} from '../util';
+
+type Options = [];
+type MessageIds =
+  | 'unnecessaryTypeConversion'
+  | 'unnecessaryTypeConversionSuggestion';
+
+export default createRule<Options, MessageIds>({
+  name: 'no-unnecessary-type-conversion',
+  meta: {
+    type: 'suggestion',
+    docs: {
+      description:
+        'Disallow conversion idioms when they do not change the type or value of the expression',
+      requiresTypeChecking: true,
+    },
+    fixable: 'code',
+    hasSuggestions: true,
+    messages: {
+      unnecessaryTypeConversion:
+        '{{violation}} does not change the type or value of the {{type}}.',
+      unnecessaryTypeConversionSuggestion:
+        'Instead, assert that the value satisfies type {{type}}.',
+    },
+    schema: [],
+  },
+  defaultOptions: [],
+  create(context) {
+    function doesUnderlyingTypeMatchFlag(
+      type: ts.Type,
+      typeFlag: ts.TypeFlags,
+    ): boolean {
+      const matchesType = (t: ts.Type): boolean => {
+        return isTypeFlagSet(t, typeFlag);
+      };
+
+      if (type.isUnion()) {
+        return type.types.every(matchesType);
+      }
+
+      return matchesType(type);
+    }
+
+    const services = getParserServices(context);
+
+    function handleUnaryOperator(
+      node: TSESTree.UnaryExpression,
+      typeFlag: ts.TypeFlags,
+      reportDescriptorMessageData: ReportDescriptorMessageData,
+      isDoubleOperator: boolean, // !! or ~~
+    ) {
+      const type = services.getTypeAtLocation(node.argument);
+      if (doesUnderlyingTypeMatchFlag(type, typeFlag)) {
+        const wrappingFixerParams = {
+          node: isDoubleOperator ? node.parent : node,
+          innerNode: [node.argument],
+          sourceCode: context.sourceCode,
+        };
+        const typeString =
+          typeFlag === ts.TypeFlags.BooleanLike ? 'boolean' : 'number';
+
+        context.report({
+          loc: {
+            start: isDoubleOperator ? node.parent.loc.start : node.loc.start,
+            end: {
+              column: node.loc.start.column + 1,
+              line: node.loc.start.line,
+            },
+          },
+          messageId: 'unnecessaryTypeConversion',
+          data: reportDescriptorMessageData,
+          fix: getWrappingFixer(wrappingFixerParams),
+          suggest: [
+            {
+              messageId: 'unnecessaryTypeConversionSuggestion',
+              data: { type: reportDescriptorMessageData.type },
+              fix: getWrappingFixer({
+                ...wrappingFixerParams,
+                wrap: expr => `${expr} satisfies ${typeString}`,
+              }),
+            },
+          ],
+        });
+      }
+    }
+
+    return {
+      'AssignmentExpression[operator = "+="]'(
+        node: TSESTree.AssignmentExpression,
+      ): void {
+        if (
+          node.right.type === AST_NODE_TYPES.Literal &&
+          node.right.value === '' &&
+          doesUnderlyingTypeMatchFlag(
+            services.getTypeAtLocation(node.left),
+            ts.TypeFlags.StringLike,
+          )
+        ) {
+          const wrappingFixerParams = {
+            node,
+            innerNode: [node.left],
+            sourceCode: context.sourceCode,
+          };
+
+          context.report({
+            node,
+            messageId: 'unnecessaryTypeConversion',
+            data: {
+              type: 'string',
+              violation: "Concatenating a string with ''",
+            },
+            fix:
+              node.parent.type === AST_NODE_TYPES.ExpressionStatement
+                ? (fixer: RuleFixer): RuleFix[] => [
+                    fixer.removeRange([
+                      node.parent.range[0],
+                      node.parent.range[1],
+                    ]),
+                  ]
+                : getWrappingFixer(wrappingFixerParams),
+            suggest: [
+              {
+                messageId: 'unnecessaryTypeConversionSuggestion',
+                data: { type: 'string' },
+                fix: getWrappingFixer({
+                  ...wrappingFixerParams,
+                  wrap: expr => `${expr} satisfies string`,
+                }),
+              },
+            ],
+          });
+        }
+      },
+      'BinaryExpression[operator = "+"]'(
+        node: TSESTree.BinaryExpression,
+      ): void {
+        if (
+          node.right.type === AST_NODE_TYPES.Literal &&
+          node.right.value === '' &&
+          doesUnderlyingTypeMatchFlag(
+            services.getTypeAtLocation(node.left),
+            ts.TypeFlags.StringLike,
+          )
+        ) {
+          const wrappingFixerParams = {
+            node,
+            innerNode: [node.left],
+            sourceCode: context.sourceCode,
+          };
+
+          context.report({
+            loc: {
+              start: node.left.loc.end,
+              end: node.loc.end,
+            },
+            messageId: 'unnecessaryTypeConversion',
+            data: {
+              type: 'string',
+              violation: "Concatenating a string with ''",
+            },
+            fix: getWrappingFixer(wrappingFixerParams),
+            suggest: [
+              {
+                messageId: 'unnecessaryTypeConversionSuggestion',
+                data: { type: 'string' },
+                fix: getWrappingFixer({
+                  ...wrappingFixerParams,
+                  wrap: expr => `${expr} satisfies string`,
+                }),
+              },
+            ],
+          });
+        } else if (
+          node.left.type === AST_NODE_TYPES.Literal &&
+          node.left.value === '' &&
+          doesUnderlyingTypeMatchFlag(
+            services.getTypeAtLocation(node.right),
+            ts.TypeFlags.StringLike,
+          )
+        ) {
+          const wrappingFixerParams = {
+            node,
+            innerNode: [node.right],
+            sourceCode: context.sourceCode,
+          };
+
+          context.report({
+            loc: {
+              start: node.loc.start,
+              end: node.right.loc.start,
+            },
+            messageId: 'unnecessaryTypeConversion',
+            data: {
+              type: 'string',
+              violation: "Concatenating '' with a string",
+            },
+            fix: getWrappingFixer(wrappingFixerParams),
+            suggest: [
+              {
+                messageId: 'unnecessaryTypeConversionSuggestion',
+                data: { type: 'string' },
+                fix: getWrappingFixer({
+                  ...wrappingFixerParams,
+                  wrap: expr => `${expr} satisfies string`,
+                }),
+              },
+            ],
+          });
+        }
+      },
+      CallExpression(node: TSESTree.CallExpression): void {
+        if (
+          node.callee.type === AST_NODE_TYPES.Identifier &&
+          node.arguments.length === 1
+        ) {
+          const getType = () =>
+            getConstrainedTypeAtLocation(services, node.arguments[0]);
+
+          const isBuiltInCall = (name: string) => {
+            if ((node.callee as TSESTree.Identifier).name === name) {
+              const scope = context.sourceCode.getScope(node);
+              const variable = scope.set.get(name);
+              return !variable?.defs.length;
+            }
+            return false;
+          };
+
+          if (
+            // eslint-disable-next-line @typescript-eslint/internal/prefer-ast-types-enum
+            (isBuiltInCall('String') &&
+              doesUnderlyingTypeMatchFlag(
+                getType(),
+                ts.TypeFlags.StringLike,
+              )) ||
+            (isBuiltInCall('Number') &&
+              doesUnderlyingTypeMatchFlag(
+                getType(),
+                ts.TypeFlags.NumberLike,
+              )) ||
+            // eslint-disable-next-line @typescript-eslint/internal/prefer-ast-types-enum
+            (isBuiltInCall('Boolean') &&
+              doesUnderlyingTypeMatchFlag(
+                getType(),
+                ts.TypeFlags.BooleanLike,
+              )) ||
+            (isBuiltInCall('BigInt') &&
+              doesUnderlyingTypeMatchFlag(getType(), ts.TypeFlags.BigIntLike))
+          ) {
+            const wrappingFixerParams = {
+              node,
+              innerNode: [node.arguments[0]],
+              sourceCode: context.sourceCode,
+            };
+            const typeString = node.callee.name.toLowerCase();
+
+            context.report({
+              loc: node.callee.loc,
+              messageId: 'unnecessaryTypeConversion',
+              data: {
+                type: node.callee.name.toLowerCase(),
+                violation: `Passing a ${typeString} to ${node.callee.name}()`,
+              },
+              fix: getWrappingFixer(wrappingFixerParams),
+              suggest: [
+                {
+                  messageId: 'unnecessaryTypeConversionSuggestion',
+                  data: { type: typeString },
+                  fix: getWrappingFixer({
+                    ...wrappingFixerParams,
+                    wrap: expr => `${expr} satisfies ${typeString}`,
+                  }),
+                },
+              ],
+            });
+          }
+        }
+      },
+      'CallExpression > MemberExpression.callee > Identifier[name = "toString"].property'(
path: packages/eslint-plugin/src/rules/no-unnecessary-type-conversion.ts
user: login: skondrashov
body: That test fails without the `.property`, which I guess is what you had in mind? Adding the test, leaving the selector for that reason.
created_at: 2025-03-23T23:10:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,467 @@
+import { RuleTester } from '@typescript-eslint/rule-tester';
+
+import rule from '../../src/rules/no-unnecessary-type-conversion';
+import { getFixturesRootDir } from '../RuleTester';
+
+const rootDir = getFixturesRootDir();
+
+const ruleTester = new RuleTester({
+  languageOptions: {
+    parserOptions: {
+      project: './tsconfig.json',
+      tsconfigRootDir: rootDir,
+    },
+  },
+});
+
+ruleTester.run('no-unnecessary-type-conversion', rule, {
+  valid: [
path: packages/eslint-plugin/tests/rules/no-unnecessary-type-conversion.test.ts
user: login: skondrashov
body: Added cases for boxed primitives and generics, but enums are elusive since I can't copy the tests for `no-unnecessary-template-expression` - there are no tests for enums there, and it has what I think is a bug as a result:
```ts
enum Foo {
  A = "A",
}

function f(foo: Foo) {
  return `${foo}`;
}

const c = `${Foo.A}`;
```
This finds autofixes for both the string templates, but applying them changes the type of `c` or the return type of `f` from `Foo` to `string` if applied. I'm not sure if this is a bug but this PR does the same thing, albeit with `String(foo)` instead of `` `${foo}` ``.
created_at: 2025-03-24T00:14:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,369 @@
+import type { TSESTree } from '@typescript-eslint/utils';
+import type {
+  ReportDescriptorMessageData,
+  RuleFix,
+  RuleFixer,
+} from '@typescript-eslint/utils/ts-eslint';
+
+import { AST_NODE_TYPES } from '@typescript-eslint/utils';
+import * as ts from 'typescript';
+
+import {
+  createRule,
+  getConstrainedTypeAtLocation,
+  getParserServices,
+  getWrappingFixer,
+  isTypeFlagSet,
+} from '../util';
+
+type Options = [];
+type MessageIds =
+  | 'unnecessaryTypeConversion'
+  | 'unnecessaryTypeConversionSuggestion';
+
+export default createRule<Options, MessageIds>({
+  name: 'no-unnecessary-type-conversion',
+  meta: {
+    type: 'suggestion',
+    docs: {
+      description:
+        'Disallow conversion idioms when they do not change the type or value of the expression',
+      requiresTypeChecking: true,
+    },
+    fixable: 'code',
+    hasSuggestions: true,
+    messages: {
+      unnecessaryTypeConversion:
+        '{{violation}} does not change the type or value of the {{type}}.',
+      unnecessaryTypeConversionSuggestion:
+        'Instead, assert that the value satisfies type {{type}}.',
+    },
+    schema: [],
+  },
+  defaultOptions: [],
+  create(context) {
+    function doesUnderlyingTypeMatchFlag(
+      type: ts.Type,
+      typeFlag: ts.TypeFlags,
+    ): boolean {
+      const matchesType = (t: ts.Type): boolean => {
+        return isTypeFlagSet(t, typeFlag);
+      };
+
+      if (type.isUnion()) {
+        return type.types.every(matchesType);
+      }
+
+      return matchesType(type);
+    }
+
+    const services = getParserServices(context);
+
+    function handleUnaryOperator(
+      node: TSESTree.UnaryExpression,
+      typeFlag: ts.TypeFlags,
+      reportDescriptorMessageData: ReportDescriptorMessageData,
+      isDoubleOperator: boolean, // !! or ~~
+    ) {
+      const type = services.getTypeAtLocation(node.argument);
+      if (doesUnderlyingTypeMatchFlag(type, typeFlag)) {
+        const wrappingFixerParams = {
+          node: isDoubleOperator ? node.parent : node,
+          innerNode: [node.argument],
+          sourceCode: context.sourceCode,
+        };
+        const typeString =
+          typeFlag === ts.TypeFlags.BooleanLike ? 'boolean' : 'number';
+
+        context.report({
+          loc: {
+            start: isDoubleOperator ? node.parent.loc.start : node.loc.start,
+            end: {
+              column: node.loc.start.column + 1,
+              line: node.loc.start.line,
+            },
+          },
+          messageId: 'unnecessaryTypeConversion',
+          data: reportDescriptorMessageData,
+          fix: getWrappingFixer(wrappingFixerParams),
+          suggest: [
+            {
+              messageId: 'unnecessaryTypeConversionSuggestion',
+              data: { type: reportDescriptorMessageData.type },
+              fix: getWrappingFixer({
+                ...wrappingFixerParams,
+                wrap: expr => `${expr} satisfies ${typeString}`,
+              }),
+            },
+          ],
+        });
+      }
+    }
+
+    return {
+      'AssignmentExpression[operator = "+="]'(
+        node: TSESTree.AssignmentExpression,
+      ): void {
+        if (
+          node.right.type === AST_NODE_TYPES.Literal &&
+          node.right.value === '' &&
+          doesUnderlyingTypeMatchFlag(
+            services.getTypeAtLocation(node.left),
+            ts.TypeFlags.StringLike,
+          )
+        ) {
+          const wrappingFixerParams = {
+            node,
+            innerNode: [node.left],
+            sourceCode: context.sourceCode,
+          };
+
+          context.report({
+            node,
+            messageId: 'unnecessaryTypeConversion',
+            data: {
+              type: 'string',
+              violation: "Concatenating a string with ''",
+            },
+            fix:
+              node.parent.type === AST_NODE_TYPES.ExpressionStatement
+                ? (fixer: RuleFixer): RuleFix[] => [
+                    fixer.removeRange([
+                      node.parent.range[0],
+                      node.parent.range[1],
+                    ]),
+                  ]
+                : getWrappingFixer(wrappingFixerParams),
+            suggest: [
+              {
+                messageId: 'unnecessaryTypeConversionSuggestion',
+                data: { type: 'string' },
+                fix: getWrappingFixer({
+                  ...wrappingFixerParams,
+                  wrap: expr => `${expr} satisfies string`,
+                }),
+              },
+            ],
+          });
+        }
+      },
+      'BinaryExpression[operator = "+"]'(
+        node: TSESTree.BinaryExpression,
+      ): void {
+        if (
+          node.right.type === AST_NODE_TYPES.Literal &&
+          node.right.value === '' &&
+          doesUnderlyingTypeMatchFlag(
+            services.getTypeAtLocation(node.left),
+            ts.TypeFlags.StringLike,
+          )
+        ) {
+          const wrappingFixerParams = {
+            node,
+            innerNode: [node.left],
+            sourceCode: context.sourceCode,
+          };
+
+          context.report({
+            loc: {
+              start: node.left.loc.end,
+              end: node.loc.end,
+            },
+            messageId: 'unnecessaryTypeConversion',
+            data: {
+              type: 'string',
+              violation: "Concatenating a string with ''",
+            },
+            fix: getWrappingFixer(wrappingFixerParams),
+            suggest: [
+              {
+                messageId: 'unnecessaryTypeConversionSuggestion',
+                data: { type: 'string' },
+                fix: getWrappingFixer({
+                  ...wrappingFixerParams,
+                  wrap: expr => `${expr} satisfies string`,
+                }),
+              },
+            ],
+          });
+        } else if (
+          node.left.type === AST_NODE_TYPES.Literal &&
+          node.left.value === '' &&
+          doesUnderlyingTypeMatchFlag(
+            services.getTypeAtLocation(node.right),
+            ts.TypeFlags.StringLike,
+          )
+        ) {
+          const wrappingFixerParams = {
+            node,
+            innerNode: [node.right],
+            sourceCode: context.sourceCode,
+          };
+
+          context.report({
+            loc: {
+              start: node.loc.start,
+              end: node.right.loc.start,
+            },
+            messageId: 'unnecessaryTypeConversion',
+            data: {
+              type: 'string',
+              violation: "Concatenating '' with a string",
+            },
+            fix: getWrappingFixer(wrappingFixerParams),
+            suggest: [
+              {
+                messageId: 'unnecessaryTypeConversionSuggestion',
+                data: { type: 'string' },
+                fix: getWrappingFixer({
+                  ...wrappingFixerParams,
+                  wrap: expr => `${expr} satisfies string`,
+                }),
+              },
+            ],
+          });
+        }
+      },
+      CallExpression(node: TSESTree.CallExpression): void {
+        if (
+          node.callee.type === AST_NODE_TYPES.Identifier &&
+          node.arguments.length === 1
+        ) {
+          const getType = () =>
+            getConstrainedTypeAtLocation(services, node.arguments[0]);
+
+          const isBuiltInCall = (name: string) => {
+            if ((node.callee as TSESTree.Identifier).name === name) {
path: packages/eslint-plugin/src/rules/no-unnecessary-type-conversion.ts
user: login: skondrashov
body: Is there an advantage of using `isNodeOfType` over `nodeCallee.type === AST_NODE_TYPES.Identifier`? Aliasing `nodeCallee` seemed to solve the problem on its own.
created_at: 2025-04-05T02:35:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,369 @@
+import type { TSESTree } from '@typescript-eslint/utils';
+import type {
+  ReportDescriptorMessageData,
+  RuleFix,
+  RuleFixer,
+} from '@typescript-eslint/utils/ts-eslint';
+
+import { AST_NODE_TYPES } from '@typescript-eslint/utils';
+import * as ts from 'typescript';
+
+import {
+  createRule,
+  getConstrainedTypeAtLocation,
+  getParserServices,
+  getWrappingFixer,
+  isTypeFlagSet,
+} from '../util';
+
+type Options = [];
+type MessageIds =
+  | 'unnecessaryTypeConversion'
+  | 'unnecessaryTypeConversionSuggestion';
+
+export default createRule<Options, MessageIds>({
+  name: 'no-unnecessary-type-conversion',
+  meta: {
+    type: 'suggestion',
+    docs: {
+      description:
+        'Disallow conversion idioms when they do not change the type or value of the expression',
+      requiresTypeChecking: true,
+    },
+    fixable: 'code',
+    hasSuggestions: true,
+    messages: {
+      unnecessaryTypeConversion:
+        '{{violation}} does not change the type or value of the {{type}}.',
+      unnecessaryTypeConversionSuggestion:
+        'Instead, assert that the value satisfies type {{type}}.',
+    },
+    schema: [],
+  },
+  defaultOptions: [],
+  create(context) {
+    function doesUnderlyingTypeMatchFlag(
+      type: ts.Type,
+      typeFlag: ts.TypeFlags,
+    ): boolean {
+      const matchesType = (t: ts.Type): boolean => {
+        return isTypeFlagSet(t, typeFlag);
+      };
+
+      if (type.isUnion()) {
+        return type.types.every(matchesType);
+      }
+
+      return matchesType(type);
+    }
+
+    const services = getParserServices(context);
+
+    function handleUnaryOperator(
+      node: TSESTree.UnaryExpression,
+      typeFlag: ts.TypeFlags,
+      reportDescriptorMessageData: ReportDescriptorMessageData,
+      isDoubleOperator: boolean, // !! or ~~
+    ) {
+      const type = services.getTypeAtLocation(node.argument);
+      if (doesUnderlyingTypeMatchFlag(type, typeFlag)) {
+        const wrappingFixerParams = {
+          node: isDoubleOperator ? node.parent : node,
+          innerNode: [node.argument],
+          sourceCode: context.sourceCode,
+        };
+        const typeString =
path: packages/eslint-plugin/src/rules/no-unnecessary-type-conversion.ts
user: login: skondrashov
body: lovely, did this almost exactly but with `typeString: string,` changed to `typeString: 'boolean' | 'number',` and the order of parameters changed so that `isDoubleOperator` is last cause it feels like it should be. I feel like you probably don't care about the order of the parameters, but let me know if you think changing the type is a bad move.
created_at: 2025-04-05T03:15:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,467 @@
+import { RuleTester } from '@typescript-eslint/rule-tester';
+
+import rule from '../../src/rules/no-unnecessary-type-conversion';
+import { getFixturesRootDir } from '../RuleTester';
+
+const rootDir = getFixturesRootDir();
+
+const ruleTester = new RuleTester({
+  languageOptions: {
+    parserOptions: {
+      project: './tsconfig.json',
+      tsconfigRootDir: rootDir,
+    },
+  },
+});
+
+ruleTester.run('no-unnecessary-type-conversion', rule, {
+  valid: [
path: packages/eslint-plugin/tests/rules/no-unnecessary-type-conversion.test.ts
user: login: kirkwaiblinger
body: Hmm. Yeah the `Foo` vs `string` thing is a bit of an annoying judgement call... let's keep the logic as-is (with `String(foo)` flagging and suggesting `foo`). 
created_at: 2025-05-01T15:03:42Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,369 @@
+import type { TSESTree } from '@typescript-eslint/utils';
+import type {
+  ReportDescriptorMessageData,
+  RuleFix,
+  RuleFixer,
+} from '@typescript-eslint/utils/ts-eslint';
+
+import { AST_NODE_TYPES } from '@typescript-eslint/utils';
+import * as ts from 'typescript';
+
+import {
+  createRule,
+  getConstrainedTypeAtLocation,
+  getParserServices,
+  getWrappingFixer,
+  isTypeFlagSet,
+} from '../util';
+
+type Options = [];
+type MessageIds =
+  | 'unnecessaryTypeConversion'
+  | 'unnecessaryTypeConversionSuggestion';
+
+export default createRule<Options, MessageIds>({
+  name: 'no-unnecessary-type-conversion',
+  meta: {
+    type: 'suggestion',
+    docs: {
+      description:
+        'Disallow conversion idioms when they do not change the type or value of the expression',
+      requiresTypeChecking: true,
+    },
+    fixable: 'code',
+    hasSuggestions: true,
+    messages: {
+      unnecessaryTypeConversion:
+        '{{violation}} does not change the type or value of the {{type}}.',
+      unnecessaryTypeConversionSuggestion:
+        'Instead, assert that the value satisfies type {{type}}.',
+    },
+    schema: [],
+  },
+  defaultOptions: [],
+  create(context) {
+    function doesUnderlyingTypeMatchFlag(
+      type: ts.Type,
+      typeFlag: ts.TypeFlags,
+    ): boolean {
+      const matchesType = (t: ts.Type): boolean => {
+        return isTypeFlagSet(t, typeFlag);
+      };
+
+      if (type.isUnion()) {
+        return type.types.every(matchesType);
+      }
+
+      return matchesType(type);
+    }
+
+    const services = getParserServices(context);
+
+    function handleUnaryOperator(
+      node: TSESTree.UnaryExpression,
+      typeFlag: ts.TypeFlags,
+      reportDescriptorMessageData: ReportDescriptorMessageData,
+      isDoubleOperator: boolean, // !! or ~~
+    ) {
+      const type = services.getTypeAtLocation(node.argument);
+      if (doesUnderlyingTypeMatchFlag(type, typeFlag)) {
+        const wrappingFixerParams = {
+          node: isDoubleOperator ? node.parent : node,
+          innerNode: [node.argument],
+          sourceCode: context.sourceCode,
+        };
+        const typeString =
+          typeFlag === ts.TypeFlags.BooleanLike ? 'boolean' : 'number';
+
+        context.report({
+          loc: {
+            start: isDoubleOperator ? node.parent.loc.start : node.loc.start,
+            end: {
+              column: node.loc.start.column + 1,
+              line: node.loc.start.line,
+            },
+          },
+          messageId: 'unnecessaryTypeConversion',
+          data: reportDescriptorMessageData,
+          fix: getWrappingFixer(wrappingFixerParams),
+          suggest: [
+            {
+              messageId: 'unnecessaryTypeConversionSuggestion',
+              data: { type: reportDescriptorMessageData.type },
+              fix: getWrappingFixer({
+                ...wrappingFixerParams,
+                wrap: expr => `${expr} satisfies ${typeString}`,
+              }),
+            },
+          ],
+        });
+      }
+    }
+
+    return {
+      'AssignmentExpression[operator = "+="]'(
+        node: TSESTree.AssignmentExpression,
+      ): void {
+        if (
+          node.right.type === AST_NODE_TYPES.Literal &&
+          node.right.value === '' &&
+          doesUnderlyingTypeMatchFlag(
+            services.getTypeAtLocation(node.left),
+            ts.TypeFlags.StringLike,
+          )
+        ) {
+          const wrappingFixerParams = {
+            node,
+            innerNode: [node.left],
+            sourceCode: context.sourceCode,
+          };
+
+          context.report({
+            node,
+            messageId: 'unnecessaryTypeConversion',
+            data: {
+              type: 'string',
+              violation: "Concatenating a string with ''",
+            },
+            fix:
+              node.parent.type === AST_NODE_TYPES.ExpressionStatement
+                ? (fixer: RuleFixer): RuleFix[] => [
+                    fixer.removeRange([
+                      node.parent.range[0],
+                      node.parent.range[1],
+                    ]),
+                  ]
+                : getWrappingFixer(wrappingFixerParams),
+            suggest: [
+              {
+                messageId: 'unnecessaryTypeConversionSuggestion',
+                data: { type: 'string' },
+                fix: getWrappingFixer({
+                  ...wrappingFixerParams,
+                  wrap: expr => `${expr} satisfies string`,
+                }),
+              },
+            ],
+          });
+        }
+      },
+      'BinaryExpression[operator = "+"]'(
+        node: TSESTree.BinaryExpression,
+      ): void {
+        if (
+          node.right.type === AST_NODE_TYPES.Literal &&
+          node.right.value === '' &&
+          doesUnderlyingTypeMatchFlag(
+            services.getTypeAtLocation(node.left),
+            ts.TypeFlags.StringLike,
+          )
+        ) {
+          const wrappingFixerParams = {
+            node,
+            innerNode: [node.left],
+            sourceCode: context.sourceCode,
+          };
+
+          context.report({
+            loc: {
+              start: node.left.loc.end,
+              end: node.loc.end,
+            },
+            messageId: 'unnecessaryTypeConversion',
+            data: {
+              type: 'string',
+              violation: "Concatenating a string with ''",
+            },
+            fix: getWrappingFixer(wrappingFixerParams),
+            suggest: [
+              {
+                messageId: 'unnecessaryTypeConversionSuggestion',
+                data: { type: 'string' },
+                fix: getWrappingFixer({
+                  ...wrappingFixerParams,
+                  wrap: expr => `${expr} satisfies string`,
+                }),
+              },
+            ],
+          });
+        } else if (
+          node.left.type === AST_NODE_TYPES.Literal &&
+          node.left.value === '' &&
+          doesUnderlyingTypeMatchFlag(
+            services.getTypeAtLocation(node.right),
+            ts.TypeFlags.StringLike,
+          )
+        ) {
+          const wrappingFixerParams = {
+            node,
+            innerNode: [node.right],
+            sourceCode: context.sourceCode,
+          };
+
+          context.report({
+            loc: {
+              start: node.loc.start,
+              end: node.right.loc.start,
+            },
+            messageId: 'unnecessaryTypeConversion',
+            data: {
+              type: 'string',
+              violation: "Concatenating '' with a string",
+            },
+            fix: getWrappingFixer(wrappingFixerParams),
+            suggest: [
+              {
+                messageId: 'unnecessaryTypeConversionSuggestion',
+                data: { type: 'string' },
+                fix: getWrappingFixer({
+                  ...wrappingFixerParams,
+                  wrap: expr => `${expr} satisfies string`,
+                }),
+              },
+            ],
+          });
+        }
+      },
+      CallExpression(node: TSESTree.CallExpression): void {
+        if (
+          node.callee.type === AST_NODE_TYPES.Identifier &&
+          node.arguments.length === 1
+        ) {
+          const getType = () =>
+            getConstrainedTypeAtLocation(services, node.arguments[0]);
+
+          const isBuiltInCall = (name: string) => {
+            if ((node.callee as TSESTree.Identifier).name === name) {
path: packages/eslint-plugin/src/rules/no-unnecessary-type-conversion.ts
user: login: kirkwaiblinger
body: Nope! checking the `.type` explicitly is totally fine too. It tends to be more useful to use the helper when checking a list of `.type`s.
created_at: 2025-05-01T15:36:16Z
author_association: MEMBER
diff_hunk: @@ -11,7 +11,7 @@ import TabItem from '@theme/TabItem';
 
 JavaScript has several idioms used to convert values to certain types. With TypeScript, it is possible to see at build time if the value is already of that type, making the conversion unnecessary.
 Performing unnecessary conversions increases visual clutter and harms code readability, so it's generally best practice to remove them if they don't change the type of an expression.
-This rule reports when a type conversion idiom is identified which does not change the type of an expression.
+This rule reports when a type conversion idiom is identified which does not change the runtime type of an expression.
path: packages/eslint-plugin/docs/rules/no-unnecessary-type-conversion.mdx
user: login: skondrashov
body: It's about the buildtime type of the expression I think more than the runtime type. The runtime type isn't necessarily the same going in vs out of the expression, and if it is it's most likely because it matches the buildtime types, which is what we are scanning.
created_at: 2025-05-01T17:10:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -11,7 +11,7 @@ import TabItem from '@theme/TabItem';
 
 JavaScript has several idioms used to convert values to certain types. With TypeScript, it is possible to see at build time if the value is already of that type, making the conversion unnecessary.
 Performing unnecessary conversions increases visual clutter and harms code readability, so it's generally best practice to remove them if they don't change the type of an expression.
-This rule reports when a type conversion idiom is identified which does not change the type of an expression.
+This rule reports when a type conversion idiom is identified which does not change the runtime type of an expression.
path: packages/eslint-plugin/docs/rules/no-unnecessary-type-conversion.mdx
user: login: kirkwaiblinger
body: Hmm, I guess it depends how you look at it. I was thinking about an example like this
```ts
function f(x: 1 | 2) {
   const n = x;
//       ^? const n: 1 | 2
}

function f(x: 1 | 2) {
   const n = Number(x);
//       ^? const n: number
}
```

The static TS type has changed as a result of the action of the `Number()` constructor (widened to `number`), but the _runtime_ types they imply are the same; both `1 | 2` and `number` imply `typeof n === 'number'`. Note for context that our docs generally assume the TS types are correct unless otherwise noted (as in the When Not To Use It section).

***

That all said, I think it's tricky enough to get the wording right that I think I'll revert it to "type" rather than make it worse by trying to be more precise 🙃 
created_at: 2025-05-02T03:18:12Z
author_association: MEMBER
diff_hunk: @@ -11,7 +11,7 @@ import TabItem from '@theme/TabItem';
 
 JavaScript has several idioms used to convert values to certain types. With TypeScript, it is possible to see at build time if the value is already of that type, making the conversion unnecessary.
 Performing unnecessary conversions increases visual clutter and harms code readability, so it's generally best practice to remove them if they don't change the type of an expression.
-This rule reports when a type conversion idiom is identified which does not change the type of an expression.
+This rule reports when a type conversion idiom is identified which does not change the runtime type of an expression.
path: packages/eslint-plugin/docs/rules/no-unnecessary-type-conversion.mdx
user: login: skondrashov
body: Yeah I don't remember cause it was a while ago, but I have a feeling I intentionally avoided saying build- or runtime for something related to all of these reasons. Thank you!
created_at: 2025-05-02T12:10:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,74 @@
+---
+description: 'Disallow conversion idioms when they do not change the type or value of the expression.'
+---
+
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+
+> 🛑 This file is source code, not the primary documentation location! 🛑
+>
+> See **https://typescript-eslint.io/rules/no-unnecessary-type-conversion** for documentation.
+
+JavaScript has several idioms used to convert values to certain types. With TypeScript, it is possible to see at build time if the value is already of that type, making the conversion unnecessary.
path: packages/eslint-plugin/docs/rules/no-unnecessary-type-conversion.mdx
user: login: JoshuaKGoldberg
body: [Docs] I think we'll want to explicitly call out what the things are in plain text, before getting to the explanation of why we don't want them -> examples of them.

```suggestion
JavaScript provides several commonly used idioms to convert values to a specific types:

* Primitive coercion (e.g. `Boolean(value)`, `String(value)`): using a built-in primitive function
* String concatenation (e.g. `value + ""`): turning a value into a string
* Unary coercion (e.g. `+value`, `!!value`): using a built-in operator

These conversions are unnecessary if the value is already that type.
```
created_at: 2025-05-02T18:32:04Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,376 @@
+import type { TSESTree } from '@typescript-eslint/utils';
+import type { RuleFix, RuleFixer } from '@typescript-eslint/utils/ts-eslint';
+
+import { AST_NODE_TYPES } from '@typescript-eslint/utils';
+import { unionTypeParts } from 'ts-api-utils';
+import * as ts from 'typescript';
+
+import {
+  createRule,
+  getConstrainedTypeAtLocation,
+  getParserServices,
+  getWrappingFixer,
+  isTypeFlagSet,
+} from '../util';
+
+type Options = [];
+type MessageIds =
+  | 'suggestRemove'
+  | 'suggestSatisfies'
+  | 'unnecessaryTypeConversion';
+
+export default createRule<Options, MessageIds>({
+  name: 'no-unnecessary-type-conversion',
+  meta: {
+    type: 'suggestion',
+    docs: {
+      description:
+        'Disallow conversion idioms when they do not change the type or value of the expression',
+      requiresTypeChecking: true,
+    },
+    fixable: 'code',
+    hasSuggestions: true,
+    messages: {
+      suggestRemove: 'Remove the type conversion.',
+      suggestSatisfies:
+        'Instead, assert that the value satisfies the {{type}} type.',
+      unnecessaryTypeConversion:
+        '{{violation}} does not change the type or value of the {{type}}.',
+    },
+    schema: [],
+  },
+  defaultOptions: [],
+  create(context) {
+    function doesUnderlyingTypeMatchFlag(
+      type: ts.Type,
+      typeFlag: ts.TypeFlags,
+    ): boolean {
+      return unionTypeParts(type).every(t => isTypeFlagSet(t, typeFlag));
+    }
+
+    const services = getParserServices(context);
+
+    function handleUnaryOperator(
+      node: TSESTree.UnaryExpression,
+      typeFlag: ts.TypeFlags,
+      typeString: 'boolean' | 'number',
+      violation: string,
+      isDoubleOperator: boolean, // !! or ~~
+    ) {
+      const outerNode = isDoubleOperator ? node.parent : node;
+      const type = services.getTypeAtLocation(node.argument);
+      if (doesUnderlyingTypeMatchFlag(type, typeFlag)) {
+        const wrappingFixerParams = {
+          node: outerNode,
+          innerNode: [node.argument],
+          sourceCode: context.sourceCode,
+        };
+
+        context.report({
+          loc: {
+            start: outerNode.loc.start,
+            end: {
+              column: node.loc.start.column + 1,
+              line: node.loc.start.line,
+            },
+          },
+          messageId: 'unnecessaryTypeConversion',
+          data: { type: typeString, violation },
+          suggest: [
+            {
+              messageId: 'suggestRemove',
+              fix: getWrappingFixer(wrappingFixerParams),
+            },
+            {
+              messageId: 'suggestSatisfies',
+              data: { type: typeString },
+              fix: getWrappingFixer({
+                ...wrappingFixerParams,
+                wrap: expr => `${expr} satisfies ${typeString}`,
+              }),
+            },
+          ],
+        });
+      }
+    }
+
+    return {
+      'AssignmentExpression[operator = "+="]'(
+        node: TSESTree.AssignmentExpression,
+      ): void {
+        if (
+          node.right.type === AST_NODE_TYPES.Literal &&
+          node.right.value === '' &&
+          doesUnderlyingTypeMatchFlag(
+            services.getTypeAtLocation(node.left),
+            ts.TypeFlags.StringLike,
+          )
+        ) {
+          const wrappingFixerParams = {
+            node,
+            innerNode: [node.left],
+            sourceCode: context.sourceCode,
+          };
+
+          context.report({
+            node,
+            messageId: 'unnecessaryTypeConversion',
+            data: {
+              type: 'string',
+              violation: "Concatenating a string with ''",
+            },
+            suggest: [
+              {
+                messageId: 'suggestRemove',
+                fix:
+                  node.parent.type === AST_NODE_TYPES.ExpressionStatement
+                    ? (fixer: RuleFixer): RuleFix[] => [
+                        fixer.removeRange([
+                          node.parent.range[0],
+                          node.parent.range[1],
+                        ]),
+                      ]
+                    : getWrappingFixer(wrappingFixerParams),
+              },
+              {
+                messageId: 'suggestSatisfies',
+                data: { type: 'string' },
+                fix: getWrappingFixer({
+                  ...wrappingFixerParams,
+                  wrap: expr => `${expr} satisfies string`,
+                }),
+              },
+            ],
+          });
+        }
+      },
+      'BinaryExpression[operator = "+"]'(
+        node: TSESTree.BinaryExpression,
+      ): void {
+        if (
+          node.right.type === AST_NODE_TYPES.Literal &&
+          node.right.value === '' &&
+          doesUnderlyingTypeMatchFlag(
+            services.getTypeAtLocation(node.left),
+            ts.TypeFlags.StringLike,
+          )
+        ) {
+          const wrappingFixerParams = {
+            node,
+            innerNode: [node.left],
+            sourceCode: context.sourceCode,
+          };
+
+          context.report({
+            loc: {
+              start: node.left.loc.end,
+              end: node.loc.end,
+            },
+            messageId: 'unnecessaryTypeConversion',
+            data: {
+              type: 'string',
+              violation: "Concatenating a string with ''",
+            },
+            suggest: [
+              {
+                messageId: 'suggestRemove',
+                fix: getWrappingFixer(wrappingFixerParams),
+              },
+              {
+                messageId: 'suggestSatisfies',
+                data: { type: 'string' },
+                fix: getWrappingFixer({
+                  ...wrappingFixerParams,
+                  wrap: expr => `${expr} satisfies string`,
+                }),
+              },
+            ],
+          });
+        } else if (
+          node.left.type === AST_NODE_TYPES.Literal &&
+          node.left.value === '' &&
+          doesUnderlyingTypeMatchFlag(
+            services.getTypeAtLocation(node.right),
+            ts.TypeFlags.StringLike,
+          )
+        ) {
+          const wrappingFixerParams = {
+            node,
+            innerNode: [node.right],
+            sourceCode: context.sourceCode,
+          };
+
+          context.report({
+            loc: {
+              start: node.loc.start,
+              end: node.right.loc.start,
+            },
+            messageId: 'unnecessaryTypeConversion',
+            data: {
+              type: 'string',
+              violation: "Concatenating '' with a string",
+            },
+            suggest: [
+              {
+                messageId: 'suggestRemove',
+                fix: getWrappingFixer(wrappingFixerParams),
+              },
+              {
+                messageId: 'suggestSatisfies',
+                data: { type: 'string' },
+                fix: getWrappingFixer({
+                  ...wrappingFixerParams,
+                  wrap: expr => `${expr} satisfies string`,
+                }),
+              },
+            ],
+          });
+        }
+      },
+      CallExpression(node: TSESTree.CallExpression): void {
+        const nodeCallee = node.callee;
+        if (
+          nodeCallee.type === AST_NODE_TYPES.Identifier &&
+          node.arguments.length === 1
+        ) {
+          const getTypeLazy = () =>
+            getConstrainedTypeAtLocation(services, node.arguments[0]);
+
+          const isBuiltInCall = (name: string) => {
+            if (nodeCallee.name === name) {
+              const scope = context.sourceCode.getScope(node);
+              const variable = scope.set.get(name);
+              return !variable?.defs.length;
+            }
+            return false;
+          };
+
+          if (
+            // eslint-disable-next-line @typescript-eslint/internal/prefer-ast-types-enum
+            (isBuiltInCall('String') &&
+              doesUnderlyingTypeMatchFlag(
+                getTypeLazy(),
+                ts.TypeFlags.StringLike,
+              )) ||
+            (isBuiltInCall('Number') &&
+              doesUnderlyingTypeMatchFlag(
+                getTypeLazy(),
+                ts.TypeFlags.NumberLike,
+              )) ||
+            // eslint-disable-next-line @typescript-eslint/internal/prefer-ast-types-enum
+            (isBuiltInCall('Boolean') &&
+              doesUnderlyingTypeMatchFlag(
+                getTypeLazy(),
+                ts.TypeFlags.BooleanLike,
+              )) ||
+            (isBuiltInCall('BigInt') &&
+              doesUnderlyingTypeMatchFlag(
+                getTypeLazy(),
+                ts.TypeFlags.BigIntLike,
+              ))
+          ) {
path: packages/eslint-plugin/src/rules/no-unnecessary-type-conversion.ts
user: login: JoshuaKGoldberg
body: [Refactor] If I'm understanding, this only needs to check the type against a built-in call at most once, right? When the name === matches?

If so: we can get rid of a good amount of the manual `... && ... && ...` checks by using a direct mapping from the name to type flags. Roughly:

```suggestion
        const nodeCallee = node.callee;
        const builtInTypeFlags = {
          BigInt: ts.TypeFlags.BigIntLike,
          Boolean: ts.TypeFlags.BooleanLike,
          Number: ts.TypeFlags.NumberLike,
          String: ts.TypeFlags.StringLike,
        };

        if (
          nodeCallee.type !== AST_NODE_TYPES.Identifier ||
          node.arguments.length !== 1 ||
          !(nodeCallee.name in builtInTypeFlags)
        ) {
          return;
        }

        const typeFlag =
          builtInTypeFlags[nodeCallee.name as keyof typeof builtInTypeFlags];
        const scope = context.sourceCode.getScope(node);
        const variable = scope.set.get(nodeCallee.name);
        if (
          !!variable?.defs.length ||
          !doesUnderlyingTypeMatchFlag(
            getConstrainedTypeAtLocation(services, node.arguments[0]),
            typeFlag,
          )
        ) {
          return;
        }
```

Now it doesn't need to keep checking `isBuiltInCall('...')` up to 4x.

A nice little bonus: even with the extra lines for early `{ return; }`s, this cuts down the total file line count from 376 to 363. 😎 
created_at: 2025-05-02T18:52:46Z
author_association: MEMBER
diff_hunk: @@ -4,7 +4,6 @@ import rule from '../../src/rules/await-thenable';
 import { getFixturesRootDir } from '../RuleTester';
 
 const rootDir = getFixturesRootDir();
-const messageId = 'await';
path: packages/eslint-plugin/tests/rules/await-thenable.test.ts
user: login: JoshuaKGoldberg
body: Nit: it would be nice to have these split out into their own PR. I'm not going to block on this though.
created_at: 2025-05-02T18:54:46Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,74 @@
+---
+description: 'Disallow conversion idioms when they do not change the type or value of the expression.'
+---
+
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+
+> 🛑 This file is source code, not the primary documentation location! 🛑
+>
+> See **https://typescript-eslint.io/rules/no-unnecessary-type-conversion** for documentation.
+
+JavaScript has several idioms used to convert values to certain types. With TypeScript, it is possible to see at build time if the value is already of that type, making the conversion unnecessary.
path: packages/eslint-plugin/docs/rules/no-unnecessary-type-conversion.mdx
user: login: JoshuaKGoldberg
body: Ooh good call on also mentioning `.toString()`
created_at: 2025-05-05T15:09:05Z
author_association: MEMBER

PR comments:
user: login: typescript-eslint[bot]
created_at: 2024-10-20T17:03:25Z
body: 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 <a href="https://opencollective.com/typescript-eslint" target="_blank" rel="noopener nofollow">https://opencollective.com/typescript-eslint</a>.
author_association: CONTRIBUTOR
user: login: netlify[bot]
created_at: 2024-10-20T17:03:37Z
body: ### <span aria-hidden="true">✅</span> Deploy Preview for *typescript-eslint* ready!


|  Name | Link |
|:-:|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | 6822a69fb7d81e5b28decf75df14ab6901911b25 |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/sites/typescript-eslint/deploys/68166b5ec9ca820008470ef2 |
|<span aria-hidden="true">😎</span> Deploy Preview | [https://deploy-preview-10182--typescript-eslint.netlify.app](https://deploy-preview-10182--typescript-eslint.netlify.app) |
|<span aria-hidden="true">📱</span> Preview on mobile | <details><summary> Toggle QR Code... </summary><br /><br />![QR Code](https://app.netlify.com/qr-code/eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1cmwiOiJodHRwczovL2RlcGxveS1wcmV2aWV3LTEwMTgyLS10eXBlc2NyaXB0LWVzbGludC5uZXRsaWZ5LmFwcCJ9.tSUs8-6nFfMRpcZmQnNO6JQZx78F0v-KKbSMo0aRKcw)<br /><br />_Use your smartphone camera to open QR code link._</details> |
|<span aria-hidden="true">![Lighthouse](https://app.netlify.com/public/external-usage/git-comments/lighthouse-sm.png)</span><br />Lighthouse | 1 paths audited<br />**Performance**: 97 (🔴 down 1 from production)<br />**Accessibility**: 100 (no change from production)<br />**Best Practices**: 100 (no change from production)<br />**SEO**: 98 (no change from production)<br />**PWA**: 80 (no change from production)<br />[View the detailed breakdown and full score reports](https://app.netlify.com/sites/typescript-eslint/deploys/68166b5ec9ca820008470ef2) |
---
<!-- [typescript-eslint Preview](https://deploy-preview-10182--typescript-eslint.netlify.app) -->
_To edit notification comments on pull requests, go to your [Netlify site configuration](https://app.netlify.com/sites/typescript-eslint/configuration/notifications#deploy-webhooks)._
author_association: NONE
user: login: nx-cloud[bot]
created_at: 2024-10-20T17:19:36Z
body: 
View your [CI Pipeline Execution ↗](https://cloud.nx.app/cipes/68166b85a835a331739f59ab?utm_source=pull-request&utm_medium=comment) for commit 6822a69fb7d81e5b28decf75df14ab6901911b25.

| Command | Status | Duration | Result |
|---------|--------|----------:|--------|
| `nx test eslint-plugin` | ✅ Succeeded | 6m 6s | [View ↗](https://cloud.nx.app/runs/OreUyYxWBF?utm_source=pull-request&utm_medium=comment) |
| `nx run eslint-plugin:test -- --coverage` | ✅ Succeeded | 6m 21s | [View ↗](https://cloud.nx.app/runs/SfbGqDOjKR?utm_source=pull-request&utm_medium=comment) |
| `nx test eslint-plugin --coverage=false` | ✅ Succeeded | 5m 5s | [View ↗](https://cloud.nx.app/runs/iNk71GsXVQ?utm_source=pull-request&utm_medium=comment) |
| `nx run-many --target=lint --exclude eslint-plugin` | ✅ Succeeded | 1m 20s | [View ↗](https://cloud.nx.app/runs/0zfRysdY9D?utm_source=pull-request&utm_medium=comment) |
| `nx test visitor-keys` | ✅ Succeeded | 1s | [View ↗](https://cloud.nx.app/runs/IQYDHQDSC0?utm_source=pull-request&utm_medium=comment) |
| `nx run types:build` | ✅ Succeeded | 1s | [View ↗](https://cloud.nx.app/runs/7DSX4CaHtT?utm_source=pull-request&utm_medium=comment) |
| `nx typecheck ast-spec` | ✅ Succeeded | 2s | [View ↗](https://cloud.nx.app/runs/aNv0ix6M7I?utm_source=pull-request&utm_medium=comment) |
| `nx run-many --target=build --exclude website --...` | ✅ Succeeded | 7s | [View ↗](https://cloud.nx.app/runs/xQaMZWis2j?utm_source=pull-request&utm_medium=comment) |
| `Additional runs (26)` | ✅ Succeeded | ... | [View ↗](https://cloud.nx.app/cipes/68166b85a835a331739f59ab?utm_source=pull-request&utm_medium=comment) |

---

☁️ [Nx Cloud](https://cloud.nx.app?utm_source=pull-request&utm_medium=comment) last updated this comment at `2025-05-03 19:28:41` UTC

<!-- NX_CLOUD_APP_COMMENT_END -->
author_association: NONE
user: login: skondrashov
created_at: 2024-10-20T17:28:00Z
body: 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.
author_association: CONTRIBUTOR
user: login: codecov[bot]
created_at: 2024-10-20T17:38:25Z
body: ## [Codecov](https://app.codecov.io/gh/typescript-eslint/typescript-eslint/pull/10182?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=typescript-eslint) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 90.90%. Comparing base [(`bca8a91`)](https://app.codecov.io/gh/typescript-eslint/typescript-eslint/commit/bca8a914b23d1c2ee07d8416f0f3b9991de85438?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=typescript-eslint) to head [(`6822a69`)](https://app.codecov.io/gh/typescript-eslint/typescript-eslint/commit/6822a69fb7d81e5b28decf75df14ab6901911b25?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=typescript-eslint).
> Report is 1 commits behind head on main.

<details><summary>Additional details and impacted files</summary>


```diff
@@            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](https://app.codecov.io/gh/typescript-eslint/typescript-eslint/pull/10182/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=typescript-eslint) | Coverage Δ | |
|---|---|---|
| [unittest](https://app.codecov.io/gh/typescript-eslint/typescript-eslint/pull/10182/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=typescript-eslint) | `90.90% <100.00%> (+0.06%)` | :arrow_up: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=typescript-eslint#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Files with missing lines](https://app.codecov.io/gh/typescript-eslint/typescript-eslint/pull/10182?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=typescript-eslint) | Coverage Δ | |
|---|---|---|
| [packages/eslint-plugin/src/configs/eslintrc/all.ts](https://app.codecov.io/gh/typescript-eslint/typescript-eslint/pull/10182?src=pr&el=tree&filepath=packages%2Feslint-plugin%2Fsrc%2Fconfigs%2Feslintrc%2Fall.ts&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=typescript-eslint#diff-cGFja2FnZXMvZXNsaW50LXBsdWdpbi9zcmMvY29uZmlncy9lc2xpbnRyYy9hbGwudHM=) | `100.00% <100.00%> (ø)` | |
| [...lugin/src/configs/eslintrc/disable-type-checked.ts](https://app.codecov.io/gh/typescript-eslint/typescript-eslint/pull/10182?src=pr&el=tree&filepath=packages%2Feslint-plugin%2Fsrc%2Fconfigs%2Feslintrc%2Fdisable-type-checked.ts&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=typescript-eslint#diff-cGFja2FnZXMvZXNsaW50LXBsdWdpbi9zcmMvY29uZmlncy9lc2xpbnRyYy9kaXNhYmxlLXR5cGUtY2hlY2tlZC50cw==) | `100.00% <100.00%> (ø)` | |
| [packages/eslint-plugin/src/configs/flat/all.ts](https://app.codecov.io/gh/typescript-eslint/typescript-eslint/pull/10182?src=pr&el=tree&filepath=packages%2Feslint-plugin%2Fsrc%2Fconfigs%2Fflat%2Fall.ts&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=typescript-eslint#diff-cGFja2FnZXMvZXNsaW50LXBsdWdpbi9zcmMvY29uZmlncy9mbGF0L2FsbC50cw==) | `100.00% <100.00%> (ø)` | |
| [...nt-plugin/src/configs/flat/disable-type-checked.ts](https://app.codecov.io/gh/typescript-eslint/typescript-eslint/pull/10182?src=pr&el=tree&filepath=packages%2Feslint-plugin%2Fsrc%2Fconfigs%2Fflat%2Fdisable-type-checked.ts&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=typescript-eslint#diff-cGFja2FnZXMvZXNsaW50LXBsdWdpbi9zcmMvY29uZmlncy9mbGF0L2Rpc2FibGUtdHlwZS1jaGVja2VkLnRz) | `100.00% <100.00%> (ø)` | |
| [...t-plugin/src/rules/adjacent-overload-signatures.ts](https://app.codecov.io/gh/typescript-eslint/typescript-eslint/pull/10182?src=pr&el=tree&filepath=packages%2Feslint-plugin%2Fsrc%2Frules%2Fadjacent-overload-signatures.ts&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=typescript-eslint#diff-cGFja2FnZXMvZXNsaW50LXBsdWdpbi9zcmMvcnVsZXMvYWRqYWNlbnQtb3ZlcmxvYWQtc2lnbmF0dXJlcy50cw==) | `100.00% <100.00%> (ø)` | |
| [...-plugin/src/rules/explicit-function-return-type.ts](https://app.codecov.io/gh/typescript-eslint/typescript-eslint/pull/10182?src=pr&el=tree&filepath=packages%2Feslint-plugin%2Fsrc%2Frules%2Fexplicit-function-return-type.ts&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=typescript-eslint#diff-cGFja2FnZXMvZXNsaW50LXBsdWdpbi9zcmMvcnVsZXMvZXhwbGljaXQtZnVuY3Rpb24tcmV0dXJuLXR5cGUudHM=) | `100.00% <100.00%> (ø)` | |
| [...ackages/eslint-plugin/src/rules/member-ordering.ts](https://app.codecov.io/gh/typescript-eslint/typescript-eslint/pull/10182?src=pr&el=tree&filepath=packages%2Feslint-plugin%2Fsrc%2Frules%2Fmember-ordering.ts&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=typescript-eslint#diff-cGFja2FnZXMvZXNsaW50LXBsdWdpbi9zcmMvcnVsZXMvbWVtYmVyLW9yZGVyaW5nLnRz) | `99.20% <100.00%> (ø)` | |
| [...slint-plugin/src/rules/no-duplicate-enum-values.ts](https://app.codecov.io/gh/typescript-eslint/typescript-eslint/pull/10182?src=pr&el=tree&filepath=packages%2Feslint-plugin%2Fsrc%2Frules%2Fno-duplicate-enum-values.ts&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=typescript-eslint#diff-cGFja2FnZXMvZXNsaW50LXBsdWdpbi9zcmMvcnVsZXMvbm8tZHVwbGljYXRlLWVudW0tdmFsdWVzLnRz) | `100.00% <100.00%> (ø)` | |
| [...ages/eslint-plugin/src/rules/no-empty-interface.ts](https://app.codecov.io/gh/typescript-eslint/typescript-eslint/pull/10182?src=pr&el=tree&filepath=packages%2Feslint-plugin%2Fsrc%2Frules%2Fno-empty-interface.ts&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=typescript-eslint#diff-cGFja2FnZXMvZXNsaW50LXBsdWdpbi9zcmMvcnVsZXMvbm8tZW1wdHktaW50ZXJmYWNlLnRz) | `100.00% <100.00%> (ø)` | |
| [...plugin/src/rules/no-redundant-type-constituents.ts](https://app.codecov.io/gh/typescript-eslint/typescript-eslint/pull/10182?src=pr&el=tree&filepath=packages%2Feslint-plugin%2Fsrc%2Frules%2Fno-redundant-type-constituents.ts&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=typescript-eslint#diff-cGFja2FnZXMvZXNsaW50LXBsdWdpbi9zcmMvcnVsZXMvbm8tcmVkdW5kYW50LXR5cGUtY29uc3RpdHVlbnRzLnRz) | `97.64% <100.00%> (ø)` | |
| ... and [5 more](https://app.codecov.io/gh/typescript-eslint/typescript-eslint/pull/10182?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=typescript-eslint) | |

</details>
<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
- :package: [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
author_association: NONE
user: login: kirkwaiblinger
created_at: 2024-10-21T06:27:26Z
body: > ```ts
> // 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.
```ts
// 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:
> 
> ```ts
> "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
```ts
declare const s: string;
s + '';
~~~~~~

!!(longExpressionThatsABoolean);
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
```
But, I really like what you've done with the more granular approach.
```ts
!!(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.
author_association: MEMBER
user: login: kirkwaiblinger
created_at: 2024-10-21T06:46:03Z
body: > 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!
author_association: MEMBER
user: login: Josh-Cena
created_at: 2024-11-03T03:48:50Z
body: > 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.
author_association: MEMBER
user: login: JoshuaKGoldberg
created_at: 2024-12-14T16:59:02Z
body: 👋 Just checking in @skondrashov - is this still something you have time and energy for? 
author_association: MEMBER
user: login: skondrashov
created_at: 2024-12-19T16:56:33Z
body: I'll have time to finish up the changes probably early in the new year!
author_association: CONTRIBUTOR
user: login: JoshuaKGoldberg
created_at: 2024-12-21T14:49:57Z
body: Awesome! I'll move this to draft then in the meantime, so we don't bug you. 🚀 
author_association: MEMBER
user: login: skondrashov
created_at: 2025-01-26T08:45:27Z
body: I addressed all of @kirkwaiblinger's feedback I think. Poor commit hygiene on a couple of the things but mostly separated into atomic commits. The only thing left on my own list is to handle the case where `.toString()` is overwritten so that it does not trigger the rule, in the vein of the other scope analysis comment. Will try to resolve that tomorrow, but essentially ready for review again.

Slightly expanded list of test cases to play with in case it helps:
```typescript
{
  // setup
  let str = "string";
  let num = 1;
  const bool = true;
  const big = BigInt(1);

  // cases that should fail
  String("hello");
  "hello".toString();
  "" + "hello";
  "hello" + "";

  String(str);
  str.toString();
  "" + str;
  str + "";
  str += "";
  "asdf" + (str += "");

  Number(1);
  +1;
  ~~1;

  Number(+"1");
  +(+"1");
  ~~+"1";

  Number(num);
  +num;
  ~~num;

  Boolean(false);
  !!false;

  Boolean(bool);
  !!bool;

  BigInt(2n);

  BigInt(big);

  String("a" + "b").length;
  ("a" + "b").toString().length;
  2 * +(2 + 2);
  2 * Number(2 + 2);
  2 * ~~(2 + 2);
  false && !!(false || true);
  false && Boolean(false || true);
  2n * BigInt(2n + 2n);

  String(str).valueOf();
  str.toString().valueOf();
  Number(num).valueOf();
  Boolean(bool).valueOf();
  BigInt(big).valueOf();
}

{
  // setup
  let str = Math.random() > 0.5 ? "string" : 1;
  let num = Math.random() > 0.5 ? 1 : "1";
  const bool = Math.random() > 0.5 ? true : 1;
  const big = Math.random() > 0.5 ? BigInt(1) : 1;

  // cases that should succeed
  String(str);
  str.toString();
  `${str}`;
  "" + str;
  str + "";
  str += "";
  new String("asdf");

  Number(num);
  +num;
  ~~num;
  ~1;

  Boolean(bool);
  !!bool;
  !false;

  BigInt(big);

  {
    function String(value: any) {
      return value;
    }
    String("asdf");
  }
}
```
author_association: CONTRIBUTOR
user: login: skondrashov
created_at: 2025-02-02T21:23:39Z
body: After latest round of fixes, I believe there are 3 things I still need to do:
1. Do scope analysis to see if `.toString()` has been overwritten before applying the rule in cases involving it.
2. Move autofixes to suggestion fixes. I would like to understand the motivation for this change, but I just didn't have time to get to it today.
3. Depending on feedback, do something with the fixes that use `getWrappingFixer` but don't need to pass a `wrap` parameter.

I left some comments in the appropriate threads. @kirkwaiblinger do you mind resolving the threads that have been resolved to your satisfaction? I don't want to do it myself in case I'm wrong in resolving something.
author_association: CONTRIBUTOR
user: login: kirkwaiblinger
created_at: 2025-02-10T03:37:38Z
body: > After latest round of fixes, I believe there are 3 things I still need to do:
> 
> 1. Do scope analysis to see if `.toString()` has been overwritten before applying the rule in cases involving it.

Let's not do this, actually. It's complex to detect, a rare situation, the only situation in which it would affect correctness the rule's report is if someone overrides `String.prototype.toString` in a way that `"foo".toString()` does not return `"foo"` - not a situation we're interested in accommodating 🙃

> 2. Move autofixes to suggestion fixes. I would like to understand the motivation for this change, but I just didn't have time to get to it today.

Sounds good 👍 On the motivation, for me:
- Removing conversions to primitives is a pretty meaningful code change and can cause breakage if the TS types are not accurate at runtime. Can unpack this more, but I'd rather be cautious and demand human intervention rather than run an autofix, which many users have configured to fix on save.
- We are providing two different approaches (with and without `satisfies`) for the fix, and the only way to express multiple fix options is with two suggestions, rather than an autofix (or providing a rule option to configure the fix - [example](https://typescript-eslint.io/rules/consistent-type-imports/#fixstyle) - like but that's totally overkill here)

> 3. Depending on feedback, do something with the fixes that use `getWrappingFixer` but don't need to pass a `wrap` parameter.

responded asking for more detail 👍 

> I left some comments in the appropriate threads. @kirkwaiblinger do you mind resolving the threads that have been resolved to your satisfaction? I don't want to do it myself in case I'm wrong in resolving something.

Will do! 👍 
author_association: MEMBER
user: login: jdufresne
created_at: 2025-03-05T13:37:24Z
body: I'd like to throw out another potential case of an unnecessary type conversion: `Object.values(array)`. This is a construct that I have found in my own projects. Once found, I simplify it to be the array (or a copy of the array). If you think this is better suited for an enhancement after this version lands, I can create a new ticket to document it.

```ts
// Bad
Object.values(['a', 'b', 'c'])  // simplify to: ['a', 'b', 'c']

const array = ['a', 'b', 'c']
Object.values(array)  // simplify to: array


// Good
type MyObject = { a: string }

let arrayOrObject: array | MyObject
arrayOrObject = ['a', 'b', 'c']
Object.values(arrayOrObject)  // could be an object, can't simplify
```
author_association: NONE
user: login: kirkwaiblinger
created_at: 2025-03-11T08:27:53Z
body: @jdufresne fun suggestion! Yeah, let's scope this as an enhancement rather than part of the initial implementation 👍 
author_association: MEMBER
user: login: jdufresne
created_at: 2025-03-11T23:48:43Z
body: > @jdufresne fun suggestion! Yeah, let's scope this as an enhancement rather than part of the initial implementation 👍

Great. Thanks. Done as https://github.com/typescript-eslint/typescript-eslint/issues/10941
author_association: NONE
user: login: skondrashov
created_at: 2025-04-05T05:08:55Z
body: @kirkwaiblinger I think I'm ready for another look.

I left some replies, but I thought I'd reply to the `getWrappingFixer` questions here. I can only think of one case where there are extra parentheses added by the current code: `'a' + 'b' + '';` -> `('a' + 'b');`. Which is annoying but harmless.

I said before in one of my comments that there are cases with double parentheses, but I couldn't find any now. Not sure if I was wrong before, but everything else I tried this time made sense. Nothing with too few parentheses and just one with too many. I found some sets of patterns that might be enlightening to play with anyway -

These patterns (and similar for other types) strip all parentheses, no matter how many you put:

```typescript
String(((('a'))));    // -> 'a';
((('a'))).toString(); // -> 'a';
((('a'))) + '';       // -> 'a';
```

If the thing being operated on isn't unary, the output gets a single set of parentheses, again regardless of how many there are. The last case here is the one that feels like it's adding extra parentheses:

```typescript
String('a' + 'b');      // -> ('a' + 'b');
('a' + 'b').toString(); // -> ('a' + 'b');
'a' + 'b' + '';         // -> ('a' + 'b');
```

Using the `satisfies` fix on these is enlightening because it places the right number of pairs of parentheses in the proper places:

```typescript
'a'.toString();                  // -> 'a' satisfies string;
'a'.toString().length;           // -> ('a' satisfies string).length;
(('a').toString()).length;       // -> ('a' satisfies string).length;
('a' + 'b').toString().length;   // -> (('a' + 'b') satisfies string).length;
(('a' + 'b').toString()).length; // -> (('a' + 'b') satisfies string).length;
```

Examples like this show that everything somehow works fine with weird double operators, parentheses, and whitespace:

```typescript
( ~ ( ~ ( 5 ) ) ); // -> ( 5 );
```

I think my main concern is whether what I did to the code in `getWrappingFixer` is acceptable, and what I can do to make it better if not.

Still to do:

1. Depending on feedback, do something with the fixes that use `getWrappingFixer` but don't need to pass a wrap parameter.
1. Figure out what to do about the incorrect fixes for enums where all the fields are the same primitive type.
1. Maybe other feedback from threads?
author_association: CONTRIBUTOR
user: login: fregante
created_at: 2025-04-25T03:23:01Z
body: Excited about this rule! I just saw a lot of `Boolean('x' in globalThis)` in a repo
author_association: CONTRIBUTOR
user: login: JoshuaKGoldberg
created_at: 2025-05-02T18:24:17Z
body: I am _salivating_ at how many existing violations were removed in this PR. 🤤 
author_association: MEMBER