Skip to content

Commit

Permalink
feat(eslint-plugin): add consistent-type-definitions rule (#463)
Browse files Browse the repository at this point in the history
Deprecates `prefer-interface`
  • Loading branch information
otofu-square authored and bradzacher committed Jun 20, 2019
1 parent 747bfcb commit ec87d06
Show file tree
Hide file tree
Showing 12 changed files with 407 additions and 11 deletions.
2 changes: 1 addition & 1 deletion packages/eslint-plugin/README.md
Expand Up @@ -131,6 +131,7 @@ Then you should add `airbnb` (or `airbnb-base`) to your `extends` section of `.e
| [`@typescript-eslint/ban-types`](./docs/rules/ban-types.md) | Enforces that types will not to be used | :heavy_check_mark: | :wrench: | |
| [`@typescript-eslint/camelcase`](./docs/rules/camelcase.md) | Enforce camelCase naming convention | :heavy_check_mark: | | |
| [`@typescript-eslint/class-name-casing`](./docs/rules/class-name-casing.md) | Require PascalCased class and interface names | :heavy_check_mark: | | |
| [`@typescript-eslint/consistent-type-definitions`](./docs/rules/consistent-type-definitions.md) | Consistent with type definition either `interface` or `type` | :heavy_check_mark: | :wrench: | |
| [`@typescript-eslint/explicit-function-return-type`](./docs/rules/explicit-function-return-type.md) | Require explicit return types on functions and class methods | :heavy_check_mark: | | |
| [`@typescript-eslint/explicit-member-accessibility`](./docs/rules/explicit-member-accessibility.md) | Require explicit accessibility modifiers on class properties and methods | :heavy_check_mark: | | |
| [`@typescript-eslint/func-call-spacing`](./docs/rules/func-call-spacing.md) | Require or disallow spacing between function identifiers and their invocations | | :wrench: | |
Expand Down Expand Up @@ -169,7 +170,6 @@ Then you should add `airbnb` (or `airbnb-base`) to your `extends` section of `.e
| [`@typescript-eslint/prefer-for-of`](./docs/rules/prefer-for-of.md) | Prefer a ‘for-of’ loop over a standard ‘for’ loop if the index is only used to access the array being iterated | | | |
| [`@typescript-eslint/prefer-function-type`](./docs/rules/prefer-function-type.md) | Use function types instead of interfaces with call signatures | | :wrench: | |
| [`@typescript-eslint/prefer-includes`](./docs/rules/prefer-includes.md) | Enforce `includes` method over `indexOf` method | | :wrench: | :thought_balloon: |
| [`@typescript-eslint/prefer-interface`](./docs/rules/prefer-interface.md) | Prefer an interface declaration over a type literal (type T = { ... }) | :heavy_check_mark: | :wrench: | |
| [`@typescript-eslint/prefer-namespace-keyword`](./docs/rules/prefer-namespace-keyword.md) | Require the use of the `namespace` keyword instead of the `module` keyword to declare custom TypeScript modules | :heavy_check_mark: | :wrench: | |
| [`@typescript-eslint/prefer-regexp-exec`](./docs/rules/prefer-regexp-exec.md) | Prefer RegExp#exec() over String#match() if no global flag is provided | | | :thought_balloon: |
| [`@typescript-eslint/prefer-string-starts-ends-with`](./docs/rules/prefer-string-starts-ends-with.md) | Enforce the use of `String#startsWith` and `String#endsWith` instead of other equivalent methods of checking substrings | | :wrench: | :thought_balloon: |
Expand Down
74 changes: 74 additions & 0 deletions packages/eslint-plugin/docs/rules/consistent-type-definitions.md
@@ -0,0 +1,74 @@
# Consistent with type definition either `interface` or `type` (consistent-type-definitions)

There are two ways to define a type.

```ts
// type alias
type T1 = {
a: string;
b: number;
};

// interface keyword
interface T2 {
a: string;
b: number;
}
```

## Options

This rule accepts one string option:

- `"interface"`: enforce using `interface`s for object type definitions.
- `"type"`: enforce using `type`s for object type definitions.

For example:

```CJSON
{
// Use type for object definitions
"@typescript-eslint/consistent-type-definitions": ["error", "type"]
}
```

## Rule Details

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

```ts
type T = { x: number };
```

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

```ts
type T = string;
type Foo = string | {};

interface T {
x: number;
}
```

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

```ts
interface T {
x: number;
}
```

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

```ts
type T = { x: number };
```

## When Not To Use It

If you specifically want to use an interface or type literal for stylistic reasons, you can disable this rule.

## Compatibility

- TSLint: [interface-over-type-literal](https://palantir.github.io/tslint/rules/interface-over-type-literal/)
4 changes: 3 additions & 1 deletion packages/eslint-plugin/docs/rules/prefer-interface.md
@@ -1,7 +1,9 @@
# Prefer an interface declaration over a type literal (type T = { ... }) (prefer-interface)
# Prefer an interface declaration over a type literal (type T = { ... }) (prefer-interface)\

Interfaces are generally preferred over type literals because interfaces can be implemented, extended and merged.

## DEPRECATED - this rule has been deprecated in favour of [`consistent-type-definitions`](./consistent-type-definitions.md)

## Rule Details

Examples of **incorrect** code for this rule.
Expand Down
4 changes: 3 additions & 1 deletion packages/eslint-plugin/src/configs/all.json
Expand Up @@ -9,6 +9,7 @@
"camelcase": "off",
"@typescript-eslint/camelcase": "error",
"@typescript-eslint/class-name-casing": "error",
"@typescript-eslint/consistent-type-definitions": "error",
"@typescript-eslint/explicit-function-return-type": "error",
"@typescript-eslint/explicit-member-accessibility": "error",
"func-call-spacing": "off",
Expand All @@ -23,11 +24,13 @@
"@typescript-eslint/no-angle-bracket-type-assertion": "error",
"no-array-constructor": "off",
"@typescript-eslint/no-array-constructor": "error",
"@typescript-eslint/no-empty-function": "error",
"@typescript-eslint/no-empty-interface": "error",
"@typescript-eslint/no-explicit-any": "error",
"no-extra-parens": "off",
"@typescript-eslint/no-extra-parens": "error",
"@typescript-eslint/no-extraneous-class": "error",
"@typescript-eslint/no-floating-promises": "error",
"@typescript-eslint/no-for-in-array": "error",
"@typescript-eslint/no-inferrable-types": "error",
"no-magic-numbers": "off",
Expand All @@ -53,7 +56,6 @@
"@typescript-eslint/prefer-for-of": "error",
"@typescript-eslint/prefer-function-type": "error",
"@typescript-eslint/prefer-includes": "error",
"@typescript-eslint/prefer-interface": "error",
"@typescript-eslint/prefer-namespace-keyword": "error",
"@typescript-eslint/prefer-regexp-exec": "error",
"@typescript-eslint/prefer-string-starts-ends-with": "error",
Expand Down
103 changes: 103 additions & 0 deletions packages/eslint-plugin/src/rules/consistent-type-definitions.ts
@@ -0,0 +1,103 @@
import { TSESLint, TSESTree } from '@typescript-eslint/experimental-utils';
import * as util from '../util';

export default util.createRule({
name: 'consistent-type-definitions',
meta: {
type: 'suggestion',
docs: {
description:
'Consistent with type definition either `interface` or `type`',
category: 'Stylistic Issues',
recommended: 'error',
},
messages: {
interfaceOverType: 'Use an `interface` instead of a `type`',
typeOverInterface: 'Use a `type` instead of an `interface`',
},
schema: [
{
enum: ['interface', 'type'],
},
],
fixable: 'code',
},
defaultOptions: ['interface'],
create(context, [option]) {
const sourceCode = context.getSourceCode();

return {
// VariableDeclaration with kind type has only one VariableDeclarator
"TSTypeAliasDeclaration[typeAnnotation.type='TSTypeLiteral']"(
node: TSESTree.TSTypeAliasDeclaration,
) {
if (option === 'interface') {
context.report({
node: node.id,
messageId: 'interfaceOverType',
fix(fixer) {
const typeNode = node.typeParameters || node.id;
const fixes: TSESLint.RuleFix[] = [];

const firstToken = sourceCode.getFirstToken(node);
if (firstToken) {
fixes.push(fixer.replaceText(firstToken, 'interface'));
fixes.push(
fixer.replaceTextRange(
[typeNode.range[1], node.typeAnnotation.range[0]],
' ',
),
);
}

const afterToken = sourceCode.getTokenAfter(node.typeAnnotation);
if (
afterToken &&
afterToken.type === 'Punctuator' &&
afterToken.value === ';'
) {
fixes.push(fixer.remove(afterToken));
}

return fixes;
},
});
}
},
TSInterfaceDeclaration(node) {
if (option === 'type') {
context.report({
node: node.id,
messageId: 'typeOverInterface',
fix(fixer) {
const typeNode = node.typeParameters || node.id;
const fixes: TSESLint.RuleFix[] = [];

const firstToken = sourceCode.getFirstToken(node);
if (firstToken) {
fixes.push(fixer.replaceText(firstToken, 'type'));
fixes.push(
fixer.replaceTextRange(
[typeNode.range[1], node.body.range[0]],
' = ',
),
);
}

if (node.extends) {
node.extends.forEach(heritage => {
const typeIdentifier = sourceCode.getText(heritage);
fixes.push(
fixer.insertTextAfter(node.body, ` & ${typeIdentifier}`),
);
});
}

return fixes;
},
});
}
},
};
},
});
2 changes: 2 additions & 0 deletions packages/eslint-plugin/src/rules/index.ts
Expand Up @@ -5,6 +5,7 @@ import banTsIgnore from './ban-ts-ignore';
import banTypes from './ban-types';
import camelcase from './camelcase';
import classNameCasing from './class-name-casing';
import consistentTypeDefinitions from './consistent-type-definitions';
import explicitFunctionReturnType from './explicit-function-return-type';
import explicitMemberAccessibility from './explicit-member-accessibility';
import funcCallSpacing from './func-call-spacing';
Expand Down Expand Up @@ -63,6 +64,7 @@ export default {
'ban-types': banTypes,
camelcase: camelcase,
'class-name-casing': classNameCasing,
'consistent-type-definitions': consistentTypeDefinitions,
'explicit-function-return-type': explicitFunctionReturnType,
'explicit-member-accessibility': explicitMemberAccessibility,
'func-call-spacing': funcCallSpacing,
Expand Down
2 changes: 2 additions & 0 deletions packages/eslint-plugin/src/rules/prefer-interface.ts
Expand Up @@ -16,6 +16,8 @@ export default util.createRule({
interfaceOverType: 'Use an interface instead of a type literal.',
},
schema: [],
deprecated: true,
replacedBy: ['consistent-type-definitions'],
},
defaultOptions: [],
create(context) {
Expand Down

0 comments on commit ec87d06

Please sign in to comment.