Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(eslint-plugin): add consistent-indexed-object-style rule #2401

Merged
merged 11 commits into from Sep 28, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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