From 6da09565b8591bcc49221315c588359f2953eb56 Mon Sep 17 00:00:00 2001 From: Ronen Amiel Date: Mon, 5 May 2025 22:41:55 +0300 Subject: [PATCH 1/6] initial implementation --- .../rules/consistent-indexed-object-style.ts | 22 +++++++++++++++++++ .../consistent-indexed-object-style.test.ts | 7 ++++++ 2 files changed, 29 insertions(+) diff --git a/packages/eslint-plugin/src/rules/consistent-indexed-object-style.ts b/packages/eslint-plugin/src/rules/consistent-indexed-object-style.ts index 3b8130097218..775870d9206f 100644 --- a/packages/eslint-plugin/src/rules/consistent-indexed-object-style.ts +++ b/packages/eslint-plugin/src/rules/consistent-indexed-object-style.ts @@ -186,6 +186,24 @@ export default createRule({ return; } + if (node.parent.type === AST_NODE_TYPES.TSTypeAliasDeclaration) { + const parentId = node.parent.id; + + const scope = context.sourceCode.getScope(parentId); + const superVar = ASTUtils.findVariable(scope, parentId.name); + + if ( + superVar && + isDeeplyReferencingType( + node.parent, + superVar, + new Set([parentId]), + ) + ) { + return; + } + } + const constraint = node.constraint; if ( @@ -291,6 +309,10 @@ function isDeeplyReferencingType( return [node.indexType, node.objectType].some(type => isDeeplyReferencingType(type, superVar, visited), ); + case AST_NODE_TYPES.TSMappedType: + return [node.constraint, node.typeAnnotation].some( + type => type && isDeeplyReferencingType(type, superVar, visited), + ); case AST_NODE_TYPES.TSConditionalType: return [ node.checkType, diff --git a/packages/eslint-plugin/tests/rules/consistent-indexed-object-style.test.ts b/packages/eslint-plugin/tests/rules/consistent-indexed-object-style.test.ts index 5ecceba510a3..dad790aaecae 100644 --- a/packages/eslint-plugin/tests/rules/consistent-indexed-object-style.test.ts +++ b/packages/eslint-plugin/tests/rules/consistent-indexed-object-style.test.ts @@ -174,6 +174,13 @@ interface ExampleObject { [key: string]: ExampleRoot; } `, + ` +type Bar = { + [k in K]: Foo; +}; + +type Foo = Bar; + `, // Type literal 'type Foo = {};', From f158dc5696963419792ae68d72e1568408829444 Mon Sep 17 00:00:00 2001 From: Ronen Amiel Date: Mon, 5 May 2025 23:07:52 +0300 Subject: [PATCH 2/6] add tests --- .../consistent-indexed-object-style.test.ts | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/packages/eslint-plugin/tests/rules/consistent-indexed-object-style.test.ts b/packages/eslint-plugin/tests/rules/consistent-indexed-object-style.test.ts index dad790aaecae..79ea099c27bf 100644 --- a/packages/eslint-plugin/tests/rules/consistent-indexed-object-style.test.ts +++ b/packages/eslint-plugin/tests/rules/consistent-indexed-object-style.test.ts @@ -175,6 +175,11 @@ interface ExampleObject { } `, ` +type Bar = { + [k in K]: Bar; +}; + `, + ` type Bar = { [k in K]: Foo; }; @@ -653,6 +658,36 @@ type Foo2 = Record; type Foo3 = Record>; `, }, + { + code: ` +type Foos = { + [k in K]: { foo: Foo }; +}; + +type Foo = Foos; + `, + errors: [{ column: 39, line: 2, messageId: 'preferRecord' }], + output: ` +type Foos = Record; + +type Foo = Foos; + `, + }, + { + code: ` +type Foos = { + [k in K]: Foo[]; +}; + +type Foo = Foos; + `, + errors: [{ column: 39, line: 2, messageId: 'preferRecord' }], + output: ` +type Foos = Record; + +type Foo = Foos; + `, + }, // Generic { From aaf3bd6f8ffca2111a99622ff189076a36c8b179 Mon Sep 17 00:00:00 2001 From: Ronen Amiel Date: Mon, 5 May 2025 23:20:01 +0300 Subject: [PATCH 3/6] use existing logic --- .../rules/consistent-indexed-object-style.ts | 40 +++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/packages/eslint-plugin/src/rules/consistent-indexed-object-style.ts b/packages/eslint-plugin/src/rules/consistent-indexed-object-style.ts index 775870d9206f..e7f3358987b5 100644 --- a/packages/eslint-plugin/src/rules/consistent-indexed-object-style.ts +++ b/packages/eslint-plugin/src/rules/consistent-indexed-object-style.ts @@ -186,6 +186,22 @@ export default createRule({ return; } + const constraint = node.constraint; + + if ( + constraint.type === AST_NODE_TYPES.TSTypeOperator && + constraint.operator === 'keyof' && + !isParenthesized(constraint, context.sourceCode) + ) { + // This is a weird special case, since modifiers are preserved by + // the mapped type, but not by the Record type. So this type is not, + // in general, equivalent to a Record type. + return; + } + + // If the mapped type is circular, we can't convert it to a Record. + const parentId = findParentDeclaration(node)?.id; + if (node.parent.type === AST_NODE_TYPES.TSTypeAliasDeclaration) { const parentId = node.parent.id; @@ -204,30 +220,14 @@ export default createRule({ } } - const constraint = node.constraint; - - if ( - constraint.type === AST_NODE_TYPES.TSTypeOperator && - constraint.operator === 'keyof' && - !isParenthesized(constraint, context.sourceCode) - ) { - // This is a weird special case, since modifiers are preserved by - // the mapped type, but not by the Record type. So this type is not, - // in general, equivalent to a Record type. - return; - } - - // If the mapped type is circular, we can't convert it to a Record. - const parentId = findParentDeclaration(node)?.id; if (parentId) { const scope = context.sourceCode.getScope(key); const superVar = ASTUtils.findVariable(scope, parentId.name); if (superVar) { - const isCircular = superVar.references.some( - item => - item.isTypeReference && - node.range[0] <= item.identifier.range[0] && - node.range[1] >= item.identifier.range[1], + const isCircular = isDeeplyReferencingType( + node.parent, + superVar, + new Set([parentId]), ); if (isCircular) { return; From aab29e63d58c1b0e8fbd75cd4d4bb60146902654 Mon Sep 17 00:00:00 2001 From: Ronen Amiel Date: Mon, 5 May 2025 23:37:40 +0300 Subject: [PATCH 4/6] remove unrelated code --- .../rules/consistent-indexed-object-style.ts | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/packages/eslint-plugin/src/rules/consistent-indexed-object-style.ts b/packages/eslint-plugin/src/rules/consistent-indexed-object-style.ts index e7f3358987b5..e02b4413177d 100644 --- a/packages/eslint-plugin/src/rules/consistent-indexed-object-style.ts +++ b/packages/eslint-plugin/src/rules/consistent-indexed-object-style.ts @@ -202,24 +202,6 @@ export default createRule({ // If the mapped type is circular, we can't convert it to a Record. const parentId = findParentDeclaration(node)?.id; - if (node.parent.type === AST_NODE_TYPES.TSTypeAliasDeclaration) { - const parentId = node.parent.id; - - const scope = context.sourceCode.getScope(parentId); - const superVar = ASTUtils.findVariable(scope, parentId.name); - - if ( - superVar && - isDeeplyReferencingType( - node.parent, - superVar, - new Set([parentId]), - ) - ) { - return; - } - } - if (parentId) { const scope = context.sourceCode.getScope(key); const superVar = ASTUtils.findVariable(scope, parentId.name); From 5291e7fe9c4c45a5645b117fdd894e737425282f Mon Sep 17 00:00:00 2001 From: Ronen Amiel Date: Mon, 5 May 2025 23:40:59 +0300 Subject: [PATCH 5/6] remove unrelated changes --- .../eslint-plugin/src/rules/consistent-indexed-object-style.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/eslint-plugin/src/rules/consistent-indexed-object-style.ts b/packages/eslint-plugin/src/rules/consistent-indexed-object-style.ts index e02b4413177d..96920cb9c135 100644 --- a/packages/eslint-plugin/src/rules/consistent-indexed-object-style.ts +++ b/packages/eslint-plugin/src/rules/consistent-indexed-object-style.ts @@ -201,7 +201,6 @@ export default createRule({ // If the mapped type is circular, we can't convert it to a Record. const parentId = findParentDeclaration(node)?.id; - if (parentId) { const scope = context.sourceCode.getScope(key); const superVar = ASTUtils.findVariable(scope, parentId.name); From 40956af888a028a55b7315b26ce670503bb6ae8f Mon Sep 17 00:00:00 2001 From: Ronen Amiel Date: Tue, 6 May 2025 00:08:42 +0300 Subject: [PATCH 6/6] don't check type constraints in mapped types --- .../src/rules/consistent-indexed-object-style.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/eslint-plugin/src/rules/consistent-indexed-object-style.ts b/packages/eslint-plugin/src/rules/consistent-indexed-object-style.ts index 96920cb9c135..392462fc221f 100644 --- a/packages/eslint-plugin/src/rules/consistent-indexed-object-style.ts +++ b/packages/eslint-plugin/src/rules/consistent-indexed-object-style.ts @@ -291,9 +291,11 @@ function isDeeplyReferencingType( isDeeplyReferencingType(type, superVar, visited), ); case AST_NODE_TYPES.TSMappedType: - return [node.constraint, node.typeAnnotation].some( - type => type && isDeeplyReferencingType(type, superVar, visited), - ); + if (node.typeAnnotation) { + return isDeeplyReferencingType(node.typeAnnotation, superVar, visited); + } + + break; case AST_NODE_TYPES.TSConditionalType: return [ node.checkType,