Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
feat(eslint-plugin): add consistent-indexed-object-style rule (#2401)
  • Loading branch information
remcohaszing committed Sep 28, 2020
1 parent 229631e commit d7dc108
Show file tree
Hide file tree
Showing 6 changed files with 393 additions and 0 deletions.
1 change: 1 addition & 0 deletions packages/eslint-plugin/README.md
Expand Up @@ -104,6 +104,7 @@ Pro Tip: For larger codebases you may want to consider splitting our linting int
| [`@typescript-eslint/ban-tslint-comment`](./docs/rules/ban-tslint-comment.md) | Bans `// tslint:<rule-flag>` comments from being used | | :wrench: | |
| [`@typescript-eslint/ban-types`](./docs/rules/ban-types.md) | Bans specific types from being used | :heavy_check_mark: | :wrench: | |
| [`@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: | |
| [`@typescript-eslint/consistent-indexed-object-style`](./docs/rules/consistent-indexed-object-style.md) | Enforce or disallow the use of the record type | | :wrench: | |
| [`@typescript-eslint/consistent-type-assertions`](./docs/rules/consistent-type-assertions.md) | Enforces consistent usage of type assertions | | | |
| [`@typescript-eslint/consistent-type-definitions`](./docs/rules/consistent-type-definitions.md) | Consistent with type definition either `interface` or `type` | | :wrench: | |
| [`@typescript-eslint/consistent-type-imports`](./docs/rules/consistent-type-imports.md) | Enforces consistent usage of type imports | | :wrench: | |
Expand Down
@@ -0,0 +1,67 @@
# Enforce or disallow the use of the record type (`consistent-indexed-object-style`)

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:

```ts
interface Foo {
[key: string]: unknown;
}

type Foo = {
[key: string]: unknown;
};

type Foo = Record<string, unknown>;
```

## Options

- `"record"`: Set to `"record"` to only allow the `Record` type. Set to `"index-signature"` to only allow index signatures. (Defaults to `"record"`)

For example:

```CJSON
{
"@typescript-eslint/consistent-type-definitions": ["error", "index-signature"]
}
```

## Rule details

This rule enforces a consistent way to define records.

Examples of **incorrect** code with `record` option.

```ts
interface Foo {
[key: string]: unknown;
}

type Foo = {
[key: string]: unknown;
};
```

Examples of **correct** code with `record` option.

```ts
type Foo = Record<string, unknown>;
```

Examples of **incorrect** code with `index-signature` option.

```ts
type Foo = Record<string, unknown>;
```

Examples of **correct** code with `index-signature` option.

```ts
interface Foo {
[key: string]: unknown;
}

type Foo = {
[key: string]: unknown;
};
```
1 change: 1 addition & 0 deletions packages/eslint-plugin/src/configs/all.ts
Expand Up @@ -16,6 +16,7 @@ export = {
'@typescript-eslint/class-literal-property-style': 'error',
'comma-spacing': 'off',
'@typescript-eslint/comma-spacing': 'error',
'@typescript-eslint/consistent-indexed-object-style': 'error',
'@typescript-eslint/consistent-type-assertions': 'error',
'@typescript-eslint/consistent-type-definitions': 'error',
'@typescript-eslint/consistent-type-imports': 'error',
Expand Down
122 changes: 122 additions & 0 deletions packages/eslint-plugin/src/rules/consistent-indexed-object-style.ts
@@ -0,0 +1,122 @@
import { createRule } from '../util';
import {
AST_NODE_TYPES,
TSESTree,
} from '@typescript-eslint/experimental-utils';

type MessageIds = 'preferRecord' | 'preferIndexSignature';
type Options = ['record' | 'index-signature'];

export default createRule<Options, MessageIds>({
name: 'consistent-indexed-object-style',
meta: {
type: 'suggestion',
docs: {
description: 'Enforce or disallow the use of the record type',
category: 'Stylistic Issues',
// too opinionated to be recommended
recommended: false,
},
messages: {
preferRecord: 'A record is preferred over an index signature',
preferIndexSignature: 'An index signature is preferred over a record.',
},
fixable: 'code',
schema: [
{
enum: ['record', 'index-signature'],
},
],
},
defaultOptions: ['record'],
create(context) {
const sourceCode = context.getSourceCode();

if (context.options[0] === 'index-signature') {
return {
TSTypeReference(node): void {
const typeName = node.typeName;
if (typeName.type !== AST_NODE_TYPES.Identifier) {
return;
}
if (typeName.name !== 'Record') {
return;
}

const params = node.typeParameters?.params;
if (params?.length !== 2) {
return;
}

context.report({
node,
messageId: 'preferIndexSignature',
fix(fixer) {
const key = sourceCode.getText(params[0]);
const type = sourceCode.getText(params[1]);
return fixer.replaceText(node, `{ [key: ${key}]: ${type} }`);
},
});
},
};
}

function checkMembers(
members: TSESTree.TypeElement[],
node: TSESTree.Node,
prefix: string,
postfix: string,
): void {
if (members.length !== 1) {
return;
}
const [member] = members;

if (member.type !== AST_NODE_TYPES.TSIndexSignature) {
return;
}

const [parameter] = member.parameters;

if (!parameter) {
return;
}

if (parameter.type !== AST_NODE_TYPES.Identifier) {
return;
}
const keyType = parameter.typeAnnotation;
if (!keyType) {
return;
}

const valueType = member.typeAnnotation;
if (!valueType) {
return;
}

context.report({
node,
messageId: 'preferRecord',
fix(fixer) {
const key = sourceCode.getText(keyType.typeAnnotation);
const value = sourceCode.getText(valueType.typeAnnotation);
return fixer.replaceText(
node,
`${prefix}Record<${key}, ${value}>${postfix}`,
);
},
});
}

return {
TSTypeLiteral(node): void {
checkMembers(node.members, node, '', '');
},

TSInterfaceDeclaration(node): void {
checkMembers(node.body.body, node, `type ${node.id.name} = `, ';');
},
};
},
});
2 changes: 2 additions & 0 deletions packages/eslint-plugin/src/rules/index.ts
Expand Up @@ -9,6 +9,7 @@ import classLiteralPropertyStyle from './class-literal-property-style';
import commaDangle from './comma-dangle';
import commaSpacing from './comma-spacing';
import confusingNonNullAssertionLikeNotEqual from './no-confusing-non-null-assertion';
import consistentIndexedObjectStyle from './consistent-indexed-object-style';
import consistentTypeAssertions from './consistent-type-assertions';
import consistentTypeDefinitions from './consistent-type-definitions';
import consistentTypeImports from './consistent-type-imports';
Expand Down Expand Up @@ -117,6 +118,7 @@ export default {
'class-literal-property-style': classLiteralPropertyStyle,
'comma-dangle': commaDangle,
'comma-spacing': commaSpacing,
'consistent-indexed-object-style': consistentIndexedObjectStyle,
'consistent-type-assertions': consistentTypeAssertions,
'consistent-type-definitions': consistentTypeDefinitions,
'consistent-type-imports': consistentTypeImports,
Expand Down

0 comments on commit d7dc108

Please sign in to comment.