diff --git a/packages/eslint-plugin/src/rules/no-unnecessary-type-parameters.ts b/packages/eslint-plugin/src/rules/no-unnecessary-type-parameters.ts index f66f75482289..3a486639dff7 100644 --- a/packages/eslint-plugin/src/rules/no-unnecessary-type-parameters.ts +++ b/packages/eslint-plugin/src/rules/no-unnecessary-type-parameters.ts @@ -10,6 +10,7 @@ import type { MakeRequired } from '../util'; import { createRule, getParserServices, + getWrappingFixer, nullThrows, NullThrowsReasons, } from '../util'; @@ -108,7 +109,28 @@ export default createRule({ for (const reference of smTypeParameterVariable.references) { if (reference.isTypeReference) { const referenceNode = reference.identifier; - yield fixer.replaceText(referenceNode, constraintText); + const isComplexType = + constraint?.type === AST_NODE_TYPES.TSUnionType || + constraint?.type === AST_NODE_TYPES.TSIntersectionType || + constraint?.type === AST_NODE_TYPES.TSConditionalType; + const hasMatchingAncestorType = [ + AST_NODE_TYPES.TSArrayType, + AST_NODE_TYPES.TSIndexedAccessType, + AST_NODE_TYPES.TSIntersectionType, + AST_NODE_TYPES.TSUnionType, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + ].some(type => referenceNode.parent.parent!.type === type); + if (isComplexType && hasMatchingAncestorType) { + const fixResult = getWrappingFixer({ + node: referenceNode, + innerNode: constraint, + sourceCode: context.sourceCode, + wrap: constraintNode => constraintNode, + })(fixer); + yield fixResult; + } else { + yield fixer.replaceText(referenceNode, constraintText); + } } } diff --git a/packages/eslint-plugin/src/util/getWrappingFixer.ts b/packages/eslint-plugin/src/util/getWrappingFixer.ts index 26afcaa6405b..2ad20e1d4893 100644 --- a/packages/eslint-plugin/src/util/getWrappingFixer.ts +++ b/packages/eslint-plugin/src/util/getWrappingFixer.ts @@ -32,7 +32,7 @@ interface WrappingFixerParams { */ export function getWrappingFixer( params: WrappingFixerParams, -): TSESLint.ReportFixFunction { +): (fixer: TSESLint.RuleFixer) => TSESLint.RuleFix { const { node, innerNode = node, sourceCode, wrap } = params; const innerNodes = Array.isArray(innerNode) ? innerNode : [innerNode]; diff --git a/packages/eslint-plugin/tests/rules/no-unnecessary-type-parameters.test.ts b/packages/eslint-plugin/tests/rules/no-unnecessary-type-parameters.test.ts index cba7279691f3..9f8f05479c9c 100644 --- a/packages/eslint-plugin/tests/rules/no-unnecessary-type-parameters.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unnecessary-type-parameters.test.ts @@ -1744,5 +1744,172 @@ class Joiner { }, ], }, + { + code: ` +function join(els: T[]) { + return els.map(el => '' + el).join(','); +} + `, + errors: [ + { + data: { descriptor: 'function', name: 'T', uses: 'used only once' }, + messageId: 'sole', + suggestions: [ + { + messageId: 'replaceUsagesWithConstraint', + output: ` +function join(els: (string | number)[]) { + return els.map(el => '' + el).join(','); +} + `, + }, + ], + }, + ], + }, + { + code: ` +function join(els: T[]) { + return els.map(el => '' + el).join(','); +} + `, + errors: [ + { + data: { descriptor: 'function', name: 'T', uses: 'used only once' }, + messageId: 'sole', + suggestions: [ + { + messageId: 'replaceUsagesWithConstraint', + output: ` +function join(els: (string & number)[]) { + return els.map(el => '' + el).join(','); +} + `, + }, + ], + }, + ], + }, + { + code: ` +function join(els: T[]) { + return els.map(el => '' + el).join(','); +} + `, + errors: [ + { + data: { descriptor: 'function', name: 'T', uses: 'used only once' }, + messageId: 'sole', + suggestions: [ + { + messageId: 'replaceUsagesWithConstraint', + output: ` +function join(els: ((string & number) | boolean)[]) { + return els.map(el => '' + el).join(','); +} + `, + }, + ], + }, + ], + }, + { + code: noFormat` +function join(els: T[]) { + return els.map(el => '' + el).join(','); +} + `, + errors: [ + { + data: { descriptor: 'function', name: 'T', uses: 'used only once' }, + messageId: 'sole', + suggestions: [ + { + messageId: 'replaceUsagesWithConstraint', + output: ` +function join(els: (string | number)[]) { + return els.map(el => '' + el).join(','); +} + `, + }, + ], + }, + ], + }, + { + code: ` +function join(els: T['hoge'][]) { + return els.map(el => '' + el).join(','); +} + `, + errors: [ + { + data: { descriptor: 'function', name: 'T', uses: 'used only once' }, + messageId: 'sole', + suggestions: [ + { + messageId: 'replaceUsagesWithConstraint', + output: ` +function join(els: ({ hoge: string } | { hoge: number })['hoge'][]) { + return els.map(el => '' + el).join(','); +} + `, + }, + ], + }, + ], + }, + { + code: ` +type A = string; +type B = string; +type C = string; +declare function f(): T & C; + `, + errors: [ + { + data: { descriptor: 'function', name: 'T', uses: 'used only once' }, + messageId: 'sole', + suggestions: [ + { + messageId: 'replaceUsagesWithConstraint', + output: ` +type A = string; +type B = string; +type C = string; +declare function f(): (A | B) & C; + `, + }, + ], + }, + ], + }, + { + code: ` +type A = string; +type B = string; +type C = string; +type D = string; +declare function f(): T | null; + `, + errors: [ + { + data: { descriptor: 'function', name: 'T', uses: 'used only once' }, + messageId: 'sole', + suggestions: [ + { + messageId: 'replaceUsagesWithConstraint', + output: ` +type A = string; +type B = string; +type C = string; +type D = string; +declare function f(): (A extends B ? C : D) | null; + `, + }, + ], + }, + ], + }, ], });