Skip to content

Commit d7dc108

Browse files
authored
feat(eslint-plugin): add consistent-indexed-object-style rule (typescript-eslint#2401)
1 parent 229631e commit d7dc108

File tree

6 files changed

+393
-0
lines changed

6 files changed

+393
-0
lines changed

packages/eslint-plugin/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ Pro Tip: For larger codebases you may want to consider splitting our linting int
104104
| [`@typescript-eslint/ban-tslint-comment`](./docs/rules/ban-tslint-comment.md) | Bans `// tslint:<rule-flag>` comments from being used | | :wrench: | |
105105
| [`@typescript-eslint/ban-types`](./docs/rules/ban-types.md) | Bans specific types from being used | :heavy_check_mark: | :wrench: | |
106106
| [`@typescript-eslint/class-literal-property-style`](./docs/rules/class-literal-property-style.md) | Ensures that literals on classes are exposed in a consistent style | | :wrench: | |
107+
| [`@typescript-eslint/consistent-indexed-object-style`](./docs/rules/consistent-indexed-object-style.md) | Enforce or disallow the use of the record type | | :wrench: | |
107108
| [`@typescript-eslint/consistent-type-assertions`](./docs/rules/consistent-type-assertions.md) | Enforces consistent usage of type assertions | | | |
108109
| [`@typescript-eslint/consistent-type-definitions`](./docs/rules/consistent-type-definitions.md) | Consistent with type definition either `interface` or `type` | | :wrench: | |
109110
| [`@typescript-eslint/consistent-type-imports`](./docs/rules/consistent-type-imports.md) | Enforces consistent usage of type imports | | :wrench: | |
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# Enforce or disallow the use of the record type (`consistent-indexed-object-style`)
2+
3+
TypeScript supports defining object show keys can be flexible 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:
4+
5+
```ts
6+
interface Foo {
7+
[key: string]: unknown;
8+
}
9+
10+
type Foo = {
11+
[key: string]: unknown;
12+
};
13+
14+
type Foo = Record<string, unknown>;
15+
```
16+
17+
## Options
18+
19+
- `"record"`: Set to `"record"` to only allow the `Record` type. Set to `"index-signature"` to only allow index signatures. (Defaults to `"record"`)
20+
21+
For example:
22+
23+
```CJSON
24+
{
25+
"@typescript-eslint/consistent-type-definitions": ["error", "index-signature"]
26+
}
27+
```
28+
29+
## Rule details
30+
31+
This rule enforces a consistent way to define records.
32+
33+
Examples of **incorrect** code with `record` option.
34+
35+
```ts
36+
interface Foo {
37+
[key: string]: unknown;
38+
}
39+
40+
type Foo = {
41+
[key: string]: unknown;
42+
};
43+
```
44+
45+
Examples of **correct** code with `record` option.
46+
47+
```ts
48+
type Foo = Record<string, unknown>;
49+
```
50+
51+
Examples of **incorrect** code with `index-signature` option.
52+
53+
```ts
54+
type Foo = Record<string, unknown>;
55+
```
56+
57+
Examples of **correct** code with `index-signature` option.
58+
59+
```ts
60+
interface Foo {
61+
[key: string]: unknown;
62+
}
63+
64+
type Foo = {
65+
[key: string]: unknown;
66+
};
67+
```

packages/eslint-plugin/src/configs/all.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export = {
1616
'@typescript-eslint/class-literal-property-style': 'error',
1717
'comma-spacing': 'off',
1818
'@typescript-eslint/comma-spacing': 'error',
19+
'@typescript-eslint/consistent-indexed-object-style': 'error',
1920
'@typescript-eslint/consistent-type-assertions': 'error',
2021
'@typescript-eslint/consistent-type-definitions': 'error',
2122
'@typescript-eslint/consistent-type-imports': 'error',
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import { createRule } from '../util';
2+
import {
3+
AST_NODE_TYPES,
4+
TSESTree,
5+
} from '@typescript-eslint/experimental-utils';
6+
7+
type MessageIds = 'preferRecord' | 'preferIndexSignature';
8+
type Options = ['record' | 'index-signature'];
9+
10+
export default createRule<Options, MessageIds>({
11+
name: 'consistent-indexed-object-style',
12+
meta: {
13+
type: 'suggestion',
14+
docs: {
15+
description: 'Enforce or disallow the use of the record type',
16+
category: 'Stylistic Issues',
17+
// too opinionated to be recommended
18+
recommended: false,
19+
},
20+
messages: {
21+
preferRecord: 'A record is preferred over an index signature',
22+
preferIndexSignature: 'An index signature is preferred over a record.',
23+
},
24+
fixable: 'code',
25+
schema: [
26+
{
27+
enum: ['record', 'index-signature'],
28+
},
29+
],
30+
},
31+
defaultOptions: ['record'],
32+
create(context) {
33+
const sourceCode = context.getSourceCode();
34+
35+
if (context.options[0] === 'index-signature') {
36+
return {
37+
TSTypeReference(node): void {
38+
const typeName = node.typeName;
39+
if (typeName.type !== AST_NODE_TYPES.Identifier) {
40+
return;
41+
}
42+
if (typeName.name !== 'Record') {
43+
return;
44+
}
45+
46+
const params = node.typeParameters?.params;
47+
if (params?.length !== 2) {
48+
return;
49+
}
50+
51+
context.report({
52+
node,
53+
messageId: 'preferIndexSignature',
54+
fix(fixer) {
55+
const key = sourceCode.getText(params[0]);
56+
const type = sourceCode.getText(params[1]);
57+
return fixer.replaceText(node, `{ [key: ${key}]: ${type} }`);
58+
},
59+
});
60+
},
61+
};
62+
}
63+
64+
function checkMembers(
65+
members: TSESTree.TypeElement[],
66+
node: TSESTree.Node,
67+
prefix: string,
68+
postfix: string,
69+
): void {
70+
if (members.length !== 1) {
71+
return;
72+
}
73+
const [member] = members;
74+
75+
if (member.type !== AST_NODE_TYPES.TSIndexSignature) {
76+
return;
77+
}
78+
79+
const [parameter] = member.parameters;
80+
81+
if (!parameter) {
82+
return;
83+
}
84+
85+
if (parameter.type !== AST_NODE_TYPES.Identifier) {
86+
return;
87+
}
88+
const keyType = parameter.typeAnnotation;
89+
if (!keyType) {
90+
return;
91+
}
92+
93+
const valueType = member.typeAnnotation;
94+
if (!valueType) {
95+
return;
96+
}
97+
98+
context.report({
99+
node,
100+
messageId: 'preferRecord',
101+
fix(fixer) {
102+
const key = sourceCode.getText(keyType.typeAnnotation);
103+
const value = sourceCode.getText(valueType.typeAnnotation);
104+
return fixer.replaceText(
105+
node,
106+
`${prefix}Record<${key}, ${value}>${postfix}`,
107+
);
108+
},
109+
});
110+
}
111+
112+
return {
113+
TSTypeLiteral(node): void {
114+
checkMembers(node.members, node, '', '');
115+
},
116+
117+
TSInterfaceDeclaration(node): void {
118+
checkMembers(node.body.body, node, `type ${node.id.name} = `, ';');
119+
},
120+
};
121+
},
122+
});

packages/eslint-plugin/src/rules/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import classLiteralPropertyStyle from './class-literal-property-style';
99
import commaDangle from './comma-dangle';
1010
import commaSpacing from './comma-spacing';
1111
import confusingNonNullAssertionLikeNotEqual from './no-confusing-non-null-assertion';
12+
import consistentIndexedObjectStyle from './consistent-indexed-object-style';
1213
import consistentTypeAssertions from './consistent-type-assertions';
1314
import consistentTypeDefinitions from './consistent-type-definitions';
1415
import consistentTypeImports from './consistent-type-imports';
@@ -117,6 +118,7 @@ export default {
117118
'class-literal-property-style': classLiteralPropertyStyle,
118119
'comma-dangle': commaDangle,
119120
'comma-spacing': commaSpacing,
121+
'consistent-indexed-object-style': consistentIndexedObjectStyle,
120122
'consistent-type-assertions': consistentTypeAssertions,
121123
'consistent-type-definitions': consistentTypeDefinitions,
122124
'consistent-type-imports': consistentTypeImports,

0 commit comments

Comments
 (0)
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