diff --git a/packages/eslint-plugin/src/rules/no-unnecessary-type-arguments.ts b/packages/eslint-plugin/src/rules/no-unnecessary-type-arguments.ts index 318af6c6d01..294adf554ad 100644 --- a/packages/eslint-plugin/src/rules/no-unnecessary-type-arguments.ts +++ b/packages/eslint-plugin/src/rules/no-unnecessary-type-arguments.ts @@ -38,6 +38,22 @@ export default util.createRule<[], MessageIds>({ const parserServices = util.getParserServices(context); const checker = parserServices.program.getTypeChecker(); + function getTypeForComparison(type: ts.Type): { + type: ts.Type; + typeArguments: readonly ts.Type[]; + } { + if (util.isTypeReferenceType(type)) { + return { + type: type.target, + typeArguments: util.getTypeArguments(type, checker), + }; + } + return { + type, + typeArguments: [], + }; + } + function checkTSArgsAndParameters( esParameters: TSESTree.TSTypeParameterInstantiation, typeParameters: readonly ts.TypeParameterDeclaration[], @@ -49,19 +65,30 @@ export default util.createRule<[], MessageIds>({ if (!param?.default) { return; } + // TODO: would like checker.areTypesEquivalent. https://github.com/Microsoft/TypeScript/issues/13502 const defaultType = checker.getTypeAtLocation(param.default); const argTsNode = parserServices.esTreeNodeToTSNodeMap.get(arg); const argType = checker.getTypeAtLocation(argTsNode); - if (!argType.aliasSymbol && !defaultType.aliasSymbol) { - if (argType.flags !== defaultType.flags) { + // this check should handle some of the most simple cases of like strings, numbers, etc + if (defaultType !== argType) { + // For more complex types (like aliases to generic object types) - TS won't always create a + // global shared type object for the type - so we need to resort to manually comparing the + // reference type and the passed type arguments. + // Also - in case there are aliases - we need to resolve them before we do checks + const defaultTypeResolved = getTypeForComparison(defaultType); + const argTypeResolved = getTypeForComparison(argType); + if ( + // ensure the resolved type AND all the parameters are the same + defaultTypeResolved.type !== argTypeResolved.type || + defaultTypeResolved.typeArguments.length !== + argTypeResolved.typeArguments.length || + defaultTypeResolved.typeArguments.some( + (t, i) => t !== argTypeResolved.typeArguments[i], + ) + ) { return; } - } else if ( - argType.aliasSymbol !== defaultType.aliasSymbol || - argType.aliasTypeArguments !== defaultType.aliasTypeArguments - ) { - return; } context.report({ diff --git a/packages/eslint-plugin/tests/rules/no-unnecessary-type-arguments.test.ts b/packages/eslint-plugin/tests/rules/no-unnecessary-type-arguments.test.ts index dc140aade44..4005f7a54e0 100644 --- a/packages/eslint-plugin/tests/rules/no-unnecessary-type-arguments.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unnecessary-type-arguments.test.ts @@ -132,6 +132,19 @@ import { F } from './missing'; function bar() {} bar>(); `, + ` +type A = T; +type B = A; + `, + ` +type A> = T; +type B = A>; + `, + ` +type A = Map; +type B = T; +type C2 = B>; + `, ], invalid: [ { @@ -317,5 +330,101 @@ declare module 'bar' { } `, }, + { + code: ` +type A> = T; +type B = A>; + `, + errors: [ + { + line: 3, + messageId: 'unnecessaryTypeParameter', + }, + ], + output: ` +type A> = T; +type B = A; + `, + }, + { + code: ` +type A = Map; +type B = T; +type C = B; + `, + errors: [ + { + line: 4, + messageId: 'unnecessaryTypeParameter', + }, + ], + output: ` +type A = Map; +type B = T; +type C = B; + `, + }, + { + code: ` +type A = Map; +type B = T; +type C = B>; + `, + errors: [ + { + line: 4, + messageId: 'unnecessaryTypeParameter', + }, + ], + output: ` +type A = Map; +type B = T; +type C = B; + `, + }, + { + code: ` +type A = Map; +type B = Map; +type C = T; +type D = C; + `, + errors: [ + { + line: 5, + messageId: 'unnecessaryTypeParameter', + }, + ], + output: ` +type A = Map; +type B = Map; +type C = T; +type D = C; + `, + }, + { + code: ` +type A = Map; +type B = A; +type C = Map; +type D = C; +type E = T; +type F = E; + `, + errors: [ + { + line: 7, + messageId: 'unnecessaryTypeParameter', + }, + ], + output: ` +type A = Map; +type B = A; +type C = Map; +type D = C; +type E = T; +type F = E; + `, + }, ], }); diff --git a/packages/type-utils/src/predicates.ts b/packages/type-utils/src/predicates.ts index 16afbb25ea8..c3ece8aa3c7 100644 --- a/packages/type-utils/src/predicates.ts +++ b/packages/type-utils/src/predicates.ts @@ -54,6 +54,24 @@ export function isTypeUnknownType(type: ts.Type): boolean { return isTypeFlagSet(type, ts.TypeFlags.Unknown); } +// https://github.com/microsoft/TypeScript/blob/42aa18bf442c4df147e30deaf27261a41cbdc617/src/compiler/types.ts#L5157 +const Nullable = ts.TypeFlags.Undefined | ts.TypeFlags.Null; +// https://github.com/microsoft/TypeScript/blob/42aa18bf442c4df147e30deaf27261a41cbdc617/src/compiler/types.ts#L5187 +const ObjectFlagsType = + ts.TypeFlags.Any | + Nullable | + ts.TypeFlags.Never | + ts.TypeFlags.Object | + ts.TypeFlags.Union | + ts.TypeFlags.Intersection; +export function isTypeReferenceType(type: ts.Type): type is ts.TypeReference { + if ((type.flags & ObjectFlagsType) === 0) { + return false; + } + const objectTypeFlags = (type as ts.ObjectType).objectFlags; + return (objectTypeFlags & ts.ObjectFlags.Reference) !== 0; +} + /** * @returns true if the type is `any` */