Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit ec87d06

Browse files
otofu-squarebradzacher
authored andcommittedJun 20, 2019
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
 

‎packages/eslint-plugin/README.md

Lines changed: 1 addition & 1 deletion
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: |
Lines changed: 74 additions & 0 deletions
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/)

‎packages/eslint-plugin/docs/rules/prefer-interface.md

Lines changed: 3 additions & 1 deletion
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.

‎packages/eslint-plugin/src/configs/all.json

Lines changed: 3 additions & 1 deletion
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",
Lines changed: 103 additions & 0 deletions
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+
});

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

Lines changed: 2 additions & 0 deletions
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,

‎packages/eslint-plugin/src/rules/prefer-interface.ts

Lines changed: 2 additions & 0 deletions
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) {
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
import rule from '../../src/rules/consistent-type-definitions';
2+
import { RuleTester } from '../RuleTester';
3+
4+
const ruleTester = new RuleTester({
5+
parser: '@typescript-eslint/parser',
6+
});
7+
8+
ruleTester.run('consistent-type-definitions', rule, {
9+
valid: [
10+
{
11+
code: `var foo = { };`,
12+
options: ['interface'],
13+
},
14+
{
15+
code: `interface A {}`,
16+
options: ['interface'],
17+
},
18+
{
19+
code: `interface A extends B { x: number; }`,
20+
options: ['interface'],
21+
},
22+
{
23+
code: `type U = string;`,
24+
options: ['interface'],
25+
},
26+
{
27+
code: `type V = { x: number; } | { y: string; };`,
28+
options: ['interface'],
29+
},
30+
{
31+
code: `
32+
type Record<T, U> = {
33+
[K in T]: U;
34+
}
35+
`,
36+
options: ['interface'],
37+
},
38+
{
39+
code: `type V = { x: number; } | { y: string; };`,
40+
options: ['interface'],
41+
},
42+
{
43+
code: `type T = { x: number; }`,
44+
options: ['type'],
45+
},
46+
{
47+
code: `type T = { x: number; }`,
48+
options: ['type'],
49+
},
50+
{
51+
code: `type T = { x: number; }`,
52+
options: ['type'],
53+
},
54+
{
55+
code: `type A = { x: number; } & B & C;`,
56+
options: ['type'],
57+
},
58+
{
59+
code: `type A = { x: number; } & B<T1> & C<T2>;`,
60+
options: ['type'],
61+
},
62+
{
63+
code: `
64+
export type W<T> = {
65+
x: T,
66+
};
67+
`,
68+
options: ['type'],
69+
},
70+
],
71+
invalid: [
72+
{
73+
code: `type T = { x: number; };`,
74+
output: `interface T { x: number; }`,
75+
options: ['interface'],
76+
errors: [
77+
{
78+
messageId: 'interfaceOverType',
79+
line: 1,
80+
column: 6,
81+
},
82+
],
83+
},
84+
{
85+
code: `type T={ x: number; };`,
86+
output: `interface T { x: number; }`,
87+
options: ['interface'],
88+
errors: [
89+
{
90+
messageId: 'interfaceOverType',
91+
line: 1,
92+
column: 6,
93+
},
94+
],
95+
},
96+
{
97+
code: `type T= { x: number; };`,
98+
output: `interface T { x: number; }`,
99+
options: ['interface'],
100+
errors: [
101+
{
102+
messageId: 'interfaceOverType',
103+
line: 1,
104+
column: 6,
105+
},
106+
],
107+
},
108+
{
109+
code: `
110+
export type W<T> = {
111+
x: T,
112+
};
113+
`,
114+
output: `
115+
export interface W<T> {
116+
x: T,
117+
}
118+
`,
119+
options: ['interface'],
120+
errors: [
121+
{
122+
messageId: 'interfaceOverType',
123+
line: 2,
124+
column: 13,
125+
},
126+
],
127+
},
128+
{
129+
code: `interface T { x: number; }`,
130+
output: `type T = { x: number; }`,
131+
options: ['type'],
132+
errors: [
133+
{
134+
messageId: 'typeOverInterface',
135+
line: 1,
136+
column: 11,
137+
},
138+
],
139+
},
140+
{
141+
code: `interface T{ x: number; }`,
142+
output: `type T = { x: number; }`,
143+
options: ['type'],
144+
errors: [
145+
{
146+
messageId: 'typeOverInterface',
147+
line: 1,
148+
column: 11,
149+
},
150+
],
151+
},
152+
{
153+
code: `interface T { x: number; }`,
154+
output: `type T = { x: number; }`,
155+
options: ['type'],
156+
errors: [
157+
{
158+
messageId: 'typeOverInterface',
159+
line: 1,
160+
column: 11,
161+
},
162+
],
163+
},
164+
{
165+
code: `interface A extends B, C { x: number; };`,
166+
output: `type A = { x: number; } & B & C;`,
167+
options: ['type'],
168+
errors: [
169+
{
170+
messageId: 'typeOverInterface',
171+
line: 1,
172+
column: 11,
173+
},
174+
],
175+
},
176+
{
177+
code: `interface A extends B<T1>, C<T2> { x: number; };`,
178+
output: `type A = { x: number; } & B<T1> & C<T2>;`,
179+
options: ['type'],
180+
errors: [
181+
{
182+
messageId: 'typeOverInterface',
183+
line: 1,
184+
column: 11,
185+
},
186+
],
187+
},
188+
{
189+
code: `
190+
export interface W<T> {
191+
x: T,
192+
};
193+
`,
194+
output: `
195+
export type W<T> = {
196+
x: T,
197+
};
198+
`,
199+
options: ['type'],
200+
errors: [
201+
{
202+
messageId: 'typeOverInterface',
203+
line: 2,
204+
column: 18,
205+
},
206+
],
207+
},
208+
],
209+
});

‎packages/eslint-plugin/tools/validate-docs/check-for-rule-docs.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ import fs from 'fs';
33
import path from 'path';
44
import { logRule } from './log';
55

6-
function checkForRuleDocs<TMessageIds extends string>(
7-
rules: Record<string, TSESLint.RuleModule<TMessageIds, any, any>>,
6+
function checkForRuleDocs(
7+
rules: Record<string, Readonly<TSESLint.RuleModule<any, any, any>>>,
88
): boolean {
99
const ruleDocs = new Set(
1010
fs.readdirSync(path.resolve(__dirname, '../../docs/rules')),

‎packages/eslint-plugin/tools/validate-docs/validate-table-rules.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ import marked from 'marked';
55
import path from 'path';
66
import { logRule } from './log';
77

8-
function validateTableRules<TMessageIds extends string>(
9-
rules: Record<string, TSESLint.RuleModule<TMessageIds, any, any>>,
8+
function validateTableRules(
9+
rules: Record<string, Readonly<TSESLint.RuleModule<any, any, any>>>,
1010
rulesTable: marked.Tokens.Table,
1111
): boolean {
1212
let hasErrors = false;

‎packages/eslint-plugin/tools/validate-docs/validate-table-structure.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@ import chalk from 'chalk';
33
import marked from 'marked';
44
import { logError } from './log';
55

6-
function validateTableStructure<TMessageIds extends string>(
7-
rules: Record<string, TSESLint.RuleModule<TMessageIds, any, any>>,
6+
function validateTableStructure(
7+
rules: Record<string, Readonly<TSESLint.RuleModule<any, any, any>>>,
88
rulesTable: marked.Tokens.Table,
99
): boolean {
10-
const ruleNames = Object.keys(rules).sort();
10+
const ruleNames = Object.keys(rules)
11+
.filter(ruleName => rules[ruleName].meta.deprecated !== true)
12+
.sort();
1113
let hasErrors = false;
1214

1315
rulesTable.cells.forEach((row, rowIndex) => {

‎packages/experimental-utils/src/ts-eslint/Rule.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ interface RuleMetaData<TMessageIds extends string> {
6262
/**
6363
* The name of the rule this rule was replaced by, if it was deprecated.
6464
*/
65-
replacedBy?: string;
65+
replacedBy?: string[];
6666
/**
6767
* The options schema. Supply an empty array if there are no options.
6868
*/

0 commit comments

Comments
 (0)
Please sign in to comment.