Skip to content

Commit ec87d06

Browse files
otofu-squarebradzacher
authored andcommitted
feat(eslint-plugin): add consistent-type-definitions rule (#463)
Deprecates `prefer-interface`
1 parent 747bfcb commit ec87d06

File tree

12 files changed

+407
-11
lines changed

12 files changed

+407
-11
lines changed

Diff for: packages/eslint-plugin/README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ Then you should add `airbnb` (or `airbnb-base`) to your `extends` section of `.e
131131
| [`@typescript-eslint/ban-types`](./docs/rules/ban-types.md) | Enforces that types will not to be used | :heavy_check_mark: | :wrench: | |
132132
| [`@typescript-eslint/camelcase`](./docs/rules/camelcase.md) | Enforce camelCase naming convention | :heavy_check_mark: | | |
133133
| [`@typescript-eslint/class-name-casing`](./docs/rules/class-name-casing.md) | Require PascalCased class and interface names | :heavy_check_mark: | | |
134+
| [`@typescript-eslint/consistent-type-definitions`](./docs/rules/consistent-type-definitions.md) | Consistent with type definition either `interface` or `type` | :heavy_check_mark: | :wrench: | |
134135
| [`@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: | | |
135136
| [`@typescript-eslint/explicit-member-accessibility`](./docs/rules/explicit-member-accessibility.md) | Require explicit accessibility modifiers on class properties and methods | :heavy_check_mark: | | |
136137
| [`@typescript-eslint/func-call-spacing`](./docs/rules/func-call-spacing.md) | Require or disallow spacing between function identifiers and their invocations | | :wrench: | |
@@ -169,7 +170,6 @@ Then you should add `airbnb` (or `airbnb-base`) to your `extends` section of `.e
169170
| [`@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 | | | |
170171
| [`@typescript-eslint/prefer-function-type`](./docs/rules/prefer-function-type.md) | Use function types instead of interfaces with call signatures | | :wrench: | |
171172
| [`@typescript-eslint/prefer-includes`](./docs/rules/prefer-includes.md) | Enforce `includes` method over `indexOf` method | | :wrench: | :thought_balloon: |
172-
| [`@typescript-eslint/prefer-interface`](./docs/rules/prefer-interface.md) | Prefer an interface declaration over a type literal (type T = { ... }) | :heavy_check_mark: | :wrench: | |
173173
| [`@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: | |
174174
| [`@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: |
175175
| [`@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: |
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# Consistent with type definition either `interface` or `type` (consistent-type-definitions)
2+
3+
There are two ways to define a type.
4+
5+
```ts
6+
// type alias
7+
type T1 = {
8+
a: string;
9+
b: number;
10+
};
11+
12+
// interface keyword
13+
interface T2 {
14+
a: string;
15+
b: number;
16+
}
17+
```
18+
19+
## Options
20+
21+
This rule accepts one string option:
22+
23+
- `"interface"`: enforce using `interface`s for object type definitions.
24+
- `"type"`: enforce using `type`s for object type definitions.
25+
26+
For example:
27+
28+
```CJSON
29+
{
30+
// Use type for object definitions
31+
"@typescript-eslint/consistent-type-definitions": ["error", "type"]
32+
}
33+
```
34+
35+
## Rule Details
36+
37+
Examples of **incorrect** code with `interface` option.
38+
39+
```ts
40+
type T = { x: number };
41+
```
42+
43+
Examples of **correct** code with `interface` option.
44+
45+
```ts
46+
type T = string;
47+
type Foo = string | {};
48+
49+
interface T {
50+
x: number;
51+
}
52+
```
53+
54+
Examples of **incorrect** code with `type` option.
55+
56+
```ts
57+
interface T {
58+
x: number;
59+
}
60+
```
61+
62+
Examples of **correct** code with `type` option.
63+
64+
```ts
65+
type T = { x: number };
66+
```
67+
68+
## When Not To Use It
69+
70+
If you specifically want to use an interface or type literal for stylistic reasons, you can disable this rule.
71+
72+
## Compatibility
73+
74+
- TSLint: [interface-over-type-literal](https://palantir.github.io/tslint/rules/interface-over-type-literal/)

Diff for: packages/eslint-plugin/docs/rules/prefer-interface.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
# Prefer an interface declaration over a type literal (type T = { ... }) (prefer-interface)
1+
# Prefer an interface declaration over a type literal (type T = { ... }) (prefer-interface)\
22

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

5+
## DEPRECATED - this rule has been deprecated in favour of [`consistent-type-definitions`](./consistent-type-definitions.md)
6+
57
## Rule Details
68

79
Examples of **incorrect** code for this rule.

Diff for: packages/eslint-plugin/src/configs/all.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"camelcase": "off",
1010
"@typescript-eslint/camelcase": "error",
1111
"@typescript-eslint/class-name-casing": "error",
12+
"@typescript-eslint/consistent-type-definitions": "error",
1213
"@typescript-eslint/explicit-function-return-type": "error",
1314
"@typescript-eslint/explicit-member-accessibility": "error",
1415
"func-call-spacing": "off",
@@ -23,11 +24,13 @@
2324
"@typescript-eslint/no-angle-bracket-type-assertion": "error",
2425
"no-array-constructor": "off",
2526
"@typescript-eslint/no-array-constructor": "error",
27+
"@typescript-eslint/no-empty-function": "error",
2628
"@typescript-eslint/no-empty-interface": "error",
2729
"@typescript-eslint/no-explicit-any": "error",
2830
"no-extra-parens": "off",
2931
"@typescript-eslint/no-extra-parens": "error",
3032
"@typescript-eslint/no-extraneous-class": "error",
33+
"@typescript-eslint/no-floating-promises": "error",
3134
"@typescript-eslint/no-for-in-array": "error",
3235
"@typescript-eslint/no-inferrable-types": "error",
3336
"no-magic-numbers": "off",
@@ -53,7 +56,6 @@
5356
"@typescript-eslint/prefer-for-of": "error",
5457
"@typescript-eslint/prefer-function-type": "error",
5558
"@typescript-eslint/prefer-includes": "error",
56-
"@typescript-eslint/prefer-interface": "error",
5759
"@typescript-eslint/prefer-namespace-keyword": "error",
5860
"@typescript-eslint/prefer-regexp-exec": "error",
5961
"@typescript-eslint/prefer-string-starts-ends-with": "error",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import { TSESLint, TSESTree } from '@typescript-eslint/experimental-utils';
2+
import * as util from '../util';
3+
4+
export default util.createRule({
5+
name: 'consistent-type-definitions',
6+
meta: {
7+
type: 'suggestion',
8+
docs: {
9+
description:
10+
'Consistent with type definition either `interface` or `type`',
11+
category: 'Stylistic Issues',
12+
recommended: 'error',
13+
},
14+
messages: {
15+
interfaceOverType: 'Use an `interface` instead of a `type`',
16+
typeOverInterface: 'Use a `type` instead of an `interface`',
17+
},
18+
schema: [
19+
{
20+
enum: ['interface', 'type'],
21+
},
22+
],
23+
fixable: 'code',
24+
},
25+
defaultOptions: ['interface'],
26+
create(context, [option]) {
27+
const sourceCode = context.getSourceCode();
28+
29+
return {
30+
// VariableDeclaration with kind type has only one VariableDeclarator
31+
"TSTypeAliasDeclaration[typeAnnotation.type='TSTypeLiteral']"(
32+
node: TSESTree.TSTypeAliasDeclaration,
33+
) {
34+
if (option === 'interface') {
35+
context.report({
36+
node: node.id,
37+
messageId: 'interfaceOverType',
38+
fix(fixer) {
39+
const typeNode = node.typeParameters || node.id;
40+
const fixes: TSESLint.RuleFix[] = [];
41+
42+
const firstToken = sourceCode.getFirstToken(node);
43+
if (firstToken) {
44+
fixes.push(fixer.replaceText(firstToken, 'interface'));
45+
fixes.push(
46+
fixer.replaceTextRange(
47+
[typeNode.range[1], node.typeAnnotation.range[0]],
48+
' ',
49+
),
50+
);
51+
}
52+
53+
const afterToken = sourceCode.getTokenAfter(node.typeAnnotation);
54+
if (
55+
afterToken &&
56+
afterToken.type === 'Punctuator' &&
57+
afterToken.value === ';'
58+
) {
59+
fixes.push(fixer.remove(afterToken));
60+
}
61+
62+
return fixes;
63+
},
64+
});
65+
}
66+
},
67+
TSInterfaceDeclaration(node) {
68+
if (option === 'type') {
69+
context.report({
70+
node: node.id,
71+
messageId: 'typeOverInterface',
72+
fix(fixer) {
73+
const typeNode = node.typeParameters || node.id;
74+
const fixes: TSESLint.RuleFix[] = [];
75+
76+
const firstToken = sourceCode.getFirstToken(node);
77+
if (firstToken) {
78+
fixes.push(fixer.replaceText(firstToken, 'type'));
79+
fixes.push(
80+
fixer.replaceTextRange(
81+
[typeNode.range[1], node.body.range[0]],
82+
' = ',
83+
),
84+
);
85+
}
86+
87+
if (node.extends) {
88+
node.extends.forEach(heritage => {
89+
const typeIdentifier = sourceCode.getText(heritage);
90+
fixes.push(
91+
fixer.insertTextAfter(node.body, ` & ${typeIdentifier}`),
92+
);
93+
});
94+
}
95+
96+
return fixes;
97+
},
98+
});
99+
}
100+
},
101+
};
102+
},
103+
});

Diff for: packages/eslint-plugin/src/rules/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import banTsIgnore from './ban-ts-ignore';
55
import banTypes from './ban-types';
66
import camelcase from './camelcase';
77
import classNameCasing from './class-name-casing';
8+
import consistentTypeDefinitions from './consistent-type-definitions';
89
import explicitFunctionReturnType from './explicit-function-return-type';
910
import explicitMemberAccessibility from './explicit-member-accessibility';
1011
import funcCallSpacing from './func-call-spacing';
@@ -63,6 +64,7 @@ export default {
6364
'ban-types': banTypes,
6465
camelcase: camelcase,
6566
'class-name-casing': classNameCasing,
67+
'consistent-type-definitions': consistentTypeDefinitions,
6668
'explicit-function-return-type': explicitFunctionReturnType,
6769
'explicit-member-accessibility': explicitMemberAccessibility,
6870
'func-call-spacing': funcCallSpacing,

Diff for: packages/eslint-plugin/src/rules/prefer-interface.ts

+2
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ export default util.createRule({
1616
interfaceOverType: 'Use an interface instead of a type literal.',
1717
},
1818
schema: [],
19+
deprecated: true,
20+
replacedBy: ['consistent-type-definitions'],
1921
},
2022
defaultOptions: [],
2123
create(context) {

0 commit comments

Comments
 (0)