diff --git a/packages/eslint-plugin/docs/rules/consistent-indexed-object-style.mdx b/packages/eslint-plugin/docs/rules/consistent-indexed-object-style.mdx index 8aeb34e238d5..5c980af355b7 100644 --- a/packages/eslint-plugin/docs/rules/consistent-indexed-object-style.mdx +++ b/packages/eslint-plugin/docs/rules/consistent-indexed-object-style.mdx @@ -9,18 +9,24 @@ import TabItem from '@theme/TabItem'; > > See **https://typescript-eslint.io/rules/consistent-indexed-object-style** for documentation. -TypeScript supports defining arbitrary object keys using an index signature. TypeScript also has a builtin type named `Record` to create an empty object defining only an index signature. For example, the following types are equal: +TypeScript supports defining arbitrary object keys using an index signature or mapped type. +TypeScript also has a builtin type named `Record` to create an empty object defining only an index signature. +For example, the following types are equal: ```ts -interface Foo { +interface IndexSignatureInterface { [key: string]: unknown; } -type Foo = { +type IndexSignatureType = { [key: string]: unknown; }; -type Foo = Record; +type MappedType = { + [key in string]: unknown; +}; + +type RecordType = Record; ``` Using one declaration form consistently improves code readability. @@ -38,20 +44,24 @@ Using one declaration form consistently improves code readability. ```ts option='"record"' -interface Foo { +interface IndexSignatureInterface { [key: string]: unknown; } -type Foo = { +type IndexSignatureType = { [key: string]: unknown; }; + +type MappedType = { + [key in string]: unknown; +}; ``` ```ts option='"record"' -type Foo = Record; +type RecordType = Record; ``` @@ -63,20 +73,24 @@ type Foo = Record; ```ts option='"index-signature"' -type Foo = Record; +type RecordType = Record; ``` ```ts option='"index-signature"' -interface Foo { +interface IndexSignatureInterface { [key: string]: unknown; } -type Foo = { +type IndexSignatureType = { [key: string]: unknown; }; + +type MappedType = { + [key in string]: unknown; +}; ``` 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 ad17704bf1fb..768bdc84491a 100644 --- a/packages/eslint-plugin/src/rules/consistent-indexed-object-style.ts +++ b/packages/eslint-plugin/src/rules/consistent-indexed-object-style.ts @@ -1,8 +1,9 @@ import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; +import type { ReportFixFunction } from '@typescript-eslint/utils/ts-eslint'; import { AST_NODE_TYPES, ASTUtils } from '@typescript-eslint/utils'; -import { createRule } from '../util'; +import { createRule, isParenthesized, nullThrows } from '../util'; type MessageIds = 'preferIndexSignature' | 'preferRecord'; type Options = ['index-signature' | 'record']; @@ -142,6 +143,69 @@ export default createRule({ !node.extends.length, ); }, + TSMappedType(node): void { + const key = node.key; + const scope = context.sourceCode.getScope(key); + + const scopeManagerKey = nullThrows( + scope.variables.find( + value => value.name === key.name && value.isTypeVariable, + ), + 'key type parameter must be a defined type variable in its scope', + ); + + // If the key is used to compute the value, we can't convert to a Record. + if ( + scopeManagerKey.references.some( + reference => reference.isTypeReference, + ) + ) { + 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; + } + + // There's no builtin Mutable type, so we can't autofix it really. + const canFix = node.readonly !== '-'; + + context.report({ + node, + messageId: 'preferRecord', + ...(canFix && { + fix: (fixer): ReturnType => { + const keyType = context.sourceCode.getText(constraint); + const valueType = context.sourceCode.getText( + node.typeAnnotation, + ); + + let recordText = `Record<${keyType}, ${valueType}>`; + + if (node.optional === '+' || node.optional === true) { + recordText = `Partial<${recordText}>`; + } else if (node.optional === '-') { + recordText = `Required<${recordText}>`; + } + + if (node.readonly === '+' || node.readonly === true) { + recordText = `Readonly<${recordText}>`; + } + + return fixer.replaceText(node, recordText); + }, + }), + }); + }, TSTypeLiteral(node): void { const parent = findParentDeclaration(node); checkMembers(node.members, node, parent?.id, '', ''); diff --git a/packages/eslint-plugin/src/rules/restrict-template-expressions.ts b/packages/eslint-plugin/src/rules/restrict-template-expressions.ts index 98933d502726..3049c1fe3fcf 100644 --- a/packages/eslint-plugin/src/rules/restrict-template-expressions.ts +++ b/packages/eslint-plugin/src/rules/restrict-template-expressions.ts @@ -57,9 +57,9 @@ const optionTesters = ( tester, })); type Options = [ - { [Type in (typeof optionTesters)[number]['option']]?: boolean } & { + { allow?: TypeOrValueSpecifier[]; - }, + } & Partial>, ]; type MessageId = 'invalidType'; diff --git a/packages/eslint-plugin/src/rules/typedef.ts b/packages/eslint-plugin/src/rules/typedef.ts index 7a18ac37b572..dd70a97706ec 100644 --- a/packages/eslint-plugin/src/rules/typedef.ts +++ b/packages/eslint-plugin/src/rules/typedef.ts @@ -15,7 +15,7 @@ const enum OptionKeys { VariableDeclarationIgnoreFunction = 'variableDeclarationIgnoreFunction', } -type Options = { [k in OptionKeys]?: boolean }; +type Options = Partial>; type MessageIds = 'expectedTypedef' | 'expectedTypedefNamed'; diff --git a/packages/eslint-plugin/src/util/types.ts b/packages/eslint-plugin/src/util/types.ts index 0765b2683d6e..44166a7649dc 100644 --- a/packages/eslint-plugin/src/util/types.ts +++ b/packages/eslint-plugin/src/util/types.ts @@ -1,5 +1,4 @@ -export type MakeRequired = { - [K in Key]-?: NonNullable; -} & Omit; +export type MakeRequired = Omit & + Required>>; export type ValueOf = T[keyof T]; diff --git a/packages/eslint-plugin/tests/docs-eslint-output-snapshots/consistent-indexed-object-style.shot b/packages/eslint-plugin/tests/docs-eslint-output-snapshots/consistent-indexed-object-style.shot index 3acf9ee188aa..95c1324cb74c 100644 --- a/packages/eslint-plugin/tests/docs-eslint-output-snapshots/consistent-indexed-object-style.shot +++ b/packages/eslint-plugin/tests/docs-eslint-output-snapshots/consistent-indexed-object-style.shot @@ -4,19 +4,26 @@ exports[`Validating rule docs consistent-indexed-object-style.mdx code examples "Incorrect Options: "record" -interface Foo { -~~~~~~~~~~~~~~~ A record is preferred over an index signature. +interface IndexSignatureInterface { +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ A record is preferred over an index signature. [key: string]: unknown; ~~~~~~~~~~~~~~~~~~~~~~~~~ } ~ -type Foo = { - ~ A record is preferred over an index signature. +type IndexSignatureType = { + ~ A record is preferred over an index signature. [key: string]: unknown; ~~~~~~~~~~~~~~~~~~~~~~~~~ }; ~ + +type MappedType = { + ~ A record is preferred over an index signature. + [key in string]: unknown; +~~~~~~~~~~~~~~~~~~~~~~~~~~~ +}; +~ " `; @@ -24,7 +31,7 @@ exports[`Validating rule docs consistent-indexed-object-style.mdx code examples "Correct Options: "record" -type Foo = Record; +type RecordType = Record; " `; @@ -32,8 +39,8 @@ exports[`Validating rule docs consistent-indexed-object-style.mdx code examples "Incorrect Options: "index-signature" -type Foo = Record; - ~~~~~~~~~~~~~~~~~~~~~~~ An index signature is preferred over a record. +type RecordType = Record; + ~~~~~~~~~~~~~~~~~~~~~~~ An index signature is preferred over a record. " `; @@ -41,12 +48,16 @@ exports[`Validating rule docs consistent-indexed-object-style.mdx code examples "Correct Options: "index-signature" -interface Foo { +interface IndexSignatureInterface { [key: string]: unknown; } -type Foo = { +type IndexSignatureType = { [key: string]: unknown; }; + +type MappedType = { + [key in string]: unknown; +}; " `; 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 b6d4e8dff432..40da904bbac9 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 @@ -1,4 +1,4 @@ -import { RuleTester } from '@typescript-eslint/rule-tester'; +import { noFormat, RuleTester } from '@typescript-eslint/rule-tester'; import rule from '../../src/rules/consistent-indexed-object-style'; @@ -141,6 +141,28 @@ interface Foo { code: 'type T = A.B;', options: ['index-signature'], }, + + { + // mapped type that uses the key cannot be converted to record + code: 'type T = { [key in Foo]: key | number };', + }, + { + code: ` +function foo(e: { readonly [key in PropertyKey]-?: key }) {} + `, + }, + + { + // `in keyof` mapped types are not convertible to Record. + code: ` +function f(): { + // intentionally not using a Record to preserve optionals + [k in keyof ParseResult]: unknown; +} { + return {}; +} + `, + }, ], invalid: [ // Interface @@ -391,5 +413,161 @@ interface Foo { options: ['index-signature'], output: 'function foo(): { [key: string]: any } {}', }, + { + code: 'type T = { readonly [key in string]: number };', + errors: [{ column: 10, messageId: 'preferRecord' }], + output: `type T = Readonly>;`, + }, + { + code: 'type T = { +readonly [key in string]: number };', + errors: [{ column: 10, messageId: 'preferRecord' }], + output: `type T = Readonly>;`, + }, + { + // There is no fix, since there isn't a builtin Mutable :( + code: 'type T = { -readonly [key in string]: number };', + errors: [{ column: 10, messageId: 'preferRecord' }], + }, + { + code: 'type T = { [key in string]: number };', + errors: [{ column: 10, messageId: 'preferRecord' }], + output: `type T = Record;`, + }, + { + code: ` +function foo(e: { [key in PropertyKey]?: string }) {} + `, + errors: [ + { + column: 17, + endColumn: 50, + endLine: 2, + line: 2, + messageId: 'preferRecord', + }, + ], + output: ` +function foo(e: Partial>) {} + `, + }, + { + code: ` +function foo(e: { [key in PropertyKey]+?: string }) {} + `, + errors: [ + { + column: 17, + endColumn: 51, + endLine: 2, + line: 2, + messageId: 'preferRecord', + }, + ], + output: ` +function foo(e: Partial>) {} + `, + }, + { + code: ` +function foo(e: { [key in PropertyKey]-?: string }) {} + `, + errors: [ + { + column: 17, + endColumn: 51, + endLine: 2, + line: 2, + messageId: 'preferRecord', + }, + ], + output: ` +function foo(e: Required>) {} + `, + }, + { + code: ` +function foo(e: { readonly [key in PropertyKey]-?: string }) {} + `, + errors: [ + { + column: 17, + endColumn: 60, + endLine: 2, + line: 2, + messageId: 'preferRecord', + }, + ], + output: ` +function foo(e: Readonly>>) {} + `, + }, + { + code: ` +type Options = [ + { [Type in (typeof optionTesters)[number]['option']]?: boolean } & { + allow?: TypeOrValueSpecifier[]; + }, +]; + `, + errors: [ + { + column: 3, + endColumn: 67, + endLine: 3, + line: 3, + messageId: 'preferRecord', + }, + ], + output: ` +type Options = [ + Partial> & { + allow?: TypeOrValueSpecifier[]; + }, +]; + `, + }, + { + code: ` +export type MakeRequired = { + [K in Key]-?: NonNullable; +} & Omit; + `, + errors: [ + { + column: 58, + endColumn: 2, + endLine: 4, + line: 2, + messageId: 'preferRecord', + }, + ], + output: ` +export type MakeRequired = Required>> & Omit; + `, + }, + { + // in parenthesized expression is convertible to Record + code: noFormat` +function f(): { + [k in (keyof ParseResult)]: unknown; +} { + return {}; +} + `, + errors: [ + { + column: 15, + endColumn: 2, + endLine: 4, + line: 2, + messageId: 'preferRecord', + }, + ], + output: ` +function f(): Record { + return {}; +} + `, + }, ], }); diff --git a/packages/scope-manager/src/referencer/VisitorBase.ts b/packages/scope-manager/src/referencer/VisitorBase.ts index ad8fba8defb1..f61ac2ca2102 100644 --- a/packages/scope-manager/src/referencer/VisitorBase.ts +++ b/packages/scope-manager/src/referencer/VisitorBase.ts @@ -15,9 +15,9 @@ function isNode(node: unknown): node is TSESTree.Node { return isObject(node) && typeof node.type === 'string'; } -type NodeVisitor = { - [K in AST_NODE_TYPES]?: (node: TSESTree.Node) => void; -}; +type NodeVisitor = Partial< + Record void> +>; abstract class VisitorBase { readonly #childVisitorKeys: VisitorKeys; diff --git a/packages/website/src/theme/MDXComponents/RuleAttributes.tsx b/packages/website/src/theme/MDXComponents/RuleAttributes.tsx index 338945fd2e6d..2338ef8fd0f9 100644 --- a/packages/website/src/theme/MDXComponents/RuleAttributes.tsx +++ b/packages/website/src/theme/MDXComponents/RuleAttributes.tsx @@ -20,9 +20,8 @@ const recommendations = { stylistic: [STYLISTIC_CONFIG_EMOJI, 'stylistic'], }; -type MakeRequired = Omit & { - [K in Key]-?: NonNullable; -}; +type MakeRequired = Omit & + Required>>; type RecommendedRuleMetaDataDocs = MakeRequired< ESLintPluginDocs, pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy