eslint-plugin-interface-to-type 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
ADDED
|
File without changes
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
|
|
2
|
+
# prefer-type-over-interface
|
|
3
|
+
|
|
4
|
+
This ESLint rule enforces the use of `type` aliases instead of `interface` declarations in TypeScript. It aims to promote consistent and flexible code by leveraging the extended capabilities of `type`, especially in scenarios involving unions, intersections, or complex generic structures.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Motivation
|
|
9
|
+
|
|
10
|
+
TypeScript provides two primary ways to define structural types: `type` and `interface`. While both serve similar purposes, `type` is often more versatile and is preferred in many projects for the following reasons:
|
|
11
|
+
|
|
12
|
+
- **Union and Intersection Types:** `type` allows you to create union and intersection types directly, enhancing flexibility.
|
|
13
|
+
- **Generics:** `type` is often more concise and readable when working with generics.
|
|
14
|
+
- **Extensibility:** Using `&` with `type` makes combining and extending types more straightforward.
|
|
15
|
+
- **Consistency:** Favoring `type` over `interface` reduces cognitive overhead when reading code.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Examples
|
|
20
|
+
|
|
21
|
+
### Incorrect Code
|
|
22
|
+
❌ Using `interface`:
|
|
23
|
+
```typescript
|
|
24
|
+
interface User {
|
|
25
|
+
id: number;
|
|
26
|
+
name: string;
|
|
27
|
+
role: 'admin' | 'user';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface GenericResponse<T> {
|
|
31
|
+
data: T;
|
|
32
|
+
error?: string;
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
### Correct Code
|
|
39
|
+
✅ Using `type`:
|
|
40
|
+
```typescript
|
|
41
|
+
type User = {
|
|
42
|
+
id: number;
|
|
43
|
+
name: string;
|
|
44
|
+
role: 'admin' | 'user';
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
type GenericResponse<T> = {
|
|
48
|
+
data: T;
|
|
49
|
+
error?: string;
|
|
50
|
+
};
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Special Cases
|
|
56
|
+
|
|
57
|
+
1. **Union Types**
|
|
58
|
+
`type` is the only way to define union types:
|
|
59
|
+
```typescript
|
|
60
|
+
type Role = 'admin' | 'user' | 'guest';
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
2. **Intersection Types**
|
|
64
|
+
Use `&` to combine multiple types:
|
|
65
|
+
```typescript
|
|
66
|
+
type AdminUser = User & { permissions: string[] };
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
3. **Generics**
|
|
70
|
+
Generics are cleaner and easier to read with `type`:
|
|
71
|
+
```typescript
|
|
72
|
+
type PaginatedResponse<T> = {
|
|
73
|
+
items: T[];
|
|
74
|
+
total: number;
|
|
75
|
+
currentPage: number;
|
|
76
|
+
};
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
4. **Extending Types**
|
|
80
|
+
`type` allows you to extend multiple structures seamlessly:
|
|
81
|
+
```typescript
|
|
82
|
+
type ExtendedUser = User & { lastLogin: Date };
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## Rule Details
|
|
88
|
+
|
|
89
|
+
This rule checks all `interface` declarations and suggests converting them to `type`. It preserves the structure and intent of the original `interface` while improving consistency and flexibility in the codebase.
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## Autofix
|
|
94
|
+
|
|
95
|
+
This rule supports ESLint's `--fix` option. When enabled, it will automatically convert `interface` declarations into equivalent `type` aliases.
|
package/index.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
meta: {
|
|
3
|
+
type: 'suggestion',
|
|
4
|
+
docs: {
|
|
5
|
+
description: 'Convert interface to type',
|
|
6
|
+
category: 'Stylistic Issues',
|
|
7
|
+
recommended: false
|
|
8
|
+
},
|
|
9
|
+
fixable: 'code',
|
|
10
|
+
schema: []
|
|
11
|
+
},
|
|
12
|
+
create(context) {
|
|
13
|
+
return {
|
|
14
|
+
TSInterfaceDeclaration(node) {
|
|
15
|
+
const interfaceName = node.id.name;
|
|
16
|
+
const sourceCode = context.getSourceCode();
|
|
17
|
+
const typeParams = node.typeParameters ? sourceCode.getText(node.typeParameters) : '';
|
|
18
|
+
|
|
19
|
+
const bodyStart = node.body.range[0];
|
|
20
|
+
const bodyEnd = node.body.range[1];
|
|
21
|
+
const bodyContent = sourceCode.getText().slice(bodyStart, bodyEnd);
|
|
22
|
+
|
|
23
|
+
const parentTypes = node.extends
|
|
24
|
+
? node.extends.map((extendNode) => sourceCode.getText(extendNode)).join(' & ')
|
|
25
|
+
: '';
|
|
26
|
+
|
|
27
|
+
let typeText;
|
|
28
|
+
if (parentTypes) {
|
|
29
|
+
typeText = `type ${interfaceName}${typeParams} = ${parentTypes} & ${bodyContent};`;
|
|
30
|
+
} else {
|
|
31
|
+
typeText = `type ${interfaceName}${typeParams} = ${bodyContent};`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
context.report({
|
|
35
|
+
node,
|
|
36
|
+
message: `Interface '${interfaceName}' can be replaced by a type.`,
|
|
37
|
+
fix(fixer) {
|
|
38
|
+
return fixer.replaceText(node, typeText);
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "eslint-plugin-interface-to-type",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"main": "index.js",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"test": "mocha tests/**/*.test.js"
|
|
7
|
+
},
|
|
8
|
+
"keywords": ["eslint", "typescript", "eslint-plugin", "interface", "type"],
|
|
9
|
+
"author": "Jan Szotkowski",
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "https://github.com/janszotkowski/eslint-plugin-interface-to-type"
|
|
14
|
+
},
|
|
15
|
+
"description": "An ESLint plugin to enforce the use of type over interface in TypeScript.",
|
|
16
|
+
"dependencies": {},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@typescript-eslint/eslint-plugin": "^5.62.0",
|
|
19
|
+
"@typescript-eslint/parser": "^5.62.0",
|
|
20
|
+
"chai": "^5.1.2",
|
|
21
|
+
"eslint": "^8.57.1",
|
|
22
|
+
"eslint-rule-tester": "^2.0.0",
|
|
23
|
+
"mocha": "^10.8.2"
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
const { RuleTester } = require("eslint");
|
|
2
|
+
const rule = require("../../../lib/rules/prefer-type-over-interface");
|
|
3
|
+
|
|
4
|
+
const ruleTester = new RuleTester({
|
|
5
|
+
parser: require.resolve("@typescript-eslint/parser"),
|
|
6
|
+
parserOptions: {
|
|
7
|
+
ecmaVersion: 2020,
|
|
8
|
+
sourceType: "module",
|
|
9
|
+
},
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
ruleTester.run("prefer-type-over-interface", rule, {
|
|
13
|
+
valid: [
|
|
14
|
+
{
|
|
15
|
+
code: "type MyType = { field: string; };", // Správný kód
|
|
16
|
+
filename: "test.ts",
|
|
17
|
+
},
|
|
18
|
+
],
|
|
19
|
+
invalid: [
|
|
20
|
+
{
|
|
21
|
+
code: "interface MyInterface { field: string; }", // Chybný kód
|
|
22
|
+
filename: "test.ts",
|
|
23
|
+
errors: [{ message: "Interface 'MyInterface' can be replaced by a type." }],
|
|
24
|
+
output: "type MyInterface = { field: string; };", // Výstup se středníkem
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
code: "interface MyInterface extends Parent { field: string; }",
|
|
28
|
+
filename: "test.ts",
|
|
29
|
+
errors: [{ message: "Interface 'MyInterface' can be replaced by a type." }],
|
|
30
|
+
output: "type MyInterface = Parent & { field: string; };", // Výstup se středníkem
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
});
|