diff --git a/README.md b/README.md index d33b879..3a1d642 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,7 @@ This guarantees 100% compatibility between the plugin and the parser. | [`typescript/no-unused-vars`](./docs/rules/no-unused-vars.md) | Prevent TypeScript-specific constructs from being erroneously flagged as unused | :heavy_check_mark: | | | [`typescript/no-use-before-define`](./docs/rules/no-use-before-define.md) | Disallow the use of variables before they are defined | | | | [`typescript/no-var-requires`](./docs/rules/no-var-requires.md) | Disallows the use of require statements except in import statements (`no-var-requires` from TSLint) | | | +| [`typescript/prefer-interface`](./docs/rules/prefer-interface.md) | Prefer an interface declaration over a type literal (type T = { ... }) (`interface-over-type-literal` from TSLint) | | :wrench: | | [`typescript/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. (`no-internal-module` from TSLint) | | :wrench: | | [`typescript/type-annotation-spacing`](./docs/rules/type-annotation-spacing.md) | Require consistent spacing around type annotations (`typedef-whitespace` from TSLint) | | :wrench: | diff --git a/ROADMAP.md b/ROADMAP.md index efe53cb..f53f7c0 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -135,7 +135,7 @@ | [`file-name-casing`] | 🔌 | [`unicorn/filename-case`](https://github.com/sindresorhus/eslint-plugin-unicorn/blob/master/docs/rules/filename-case.md) | | [`import-spacing`] | 🔌 | Use [Prettier] | | [`interface-name`] | ✅ | [`typescript/interface-name-prefix`] | -| [`interface-over-type-literal`] | 🛑 | N/A | +| [`interface-over-type-literal`] | 🛑 | [`typescript/prefer-interface`] | | [`jsdoc-format`] | 🛑 | N/A | | [`match-default-export-name`] | 🛑 | N/A | | [`newline-before-return`] | 🌟 | [`padding-line-between-statements`](https://eslint.org/docs/rules/padding-line-between-statements) (`{ blankLine: "always", prev: "*", next: "return" }`) | @@ -351,4 +351,5 @@ [`typescript/no-angle-bracket-type-assertion`]: https://github.com/bradzacher/eslint-plugin-typescript/blob/master/docs/rules/no-angle-bracket-type-assertion.md [`typescript/no-parameter-properties`]: https://github.com/bradzacher/eslint-plugin-typescript/blob/master/docs/rules/no-parameter-properties.md [`typescript/member-delimiter-style`]: https://github.com/bradzacher/eslint-plugin-typescript/blob/master/docs/rules/member-delimiter-style.md +[`typescript/prefer-interface`]: https://github.com/bradzacher/eslint-plugin-typescript/blob/master/docs/rules/prefer-interface.md diff --git a/docs/rules/prefer-interface.md b/docs/rules/prefer-interface.md new file mode 100644 index 0000000..676f1bd --- /dev/null +++ b/docs/rules/prefer-interface.md @@ -0,0 +1,33 @@ +# 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. + +## Rule Details + +Examples of **incorrect** code for this rule. + +```ts +type T = { x: number; } +``` + +Examples of **correct** code for this rule. + +```ts +type T = string; +type Foo = string | { } + +interface T { + x: number; +} +``` + +## Options +```CJSON +{ + "interface-over-type-literal": "error" +} +``` + +## Compatibility + +* TSLint: [interface-over-type-literal](https://palantir.github.io/tslint/rules/interface-over-type-literal/) diff --git a/lib/rules/prefer-interface.js b/lib/rules/prefer-interface.js new file mode 100644 index 0000000..16c2e45 --- /dev/null +++ b/lib/rules/prefer-interface.js @@ -0,0 +1,72 @@ +/** + * @fileoverview Prefer an interface declaration over a type literal (type T = { ... }) + * @author Armano + */ +"use strict"; + +const util = require("../util"); + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: + "Prefer an interface declaration over a type literal (type T = { ... })", + extraDescription: [util.tslintRule("interface-over-type-literal")], + category: "TypeScript", + url: util.metaDocsUrl("prefer-interface"), + }, + fixable: "code", + messages: { + interfaceOverType: "Use an interface instead of a type literal.", + }, + schema: [], + }, + create(context) { + const sourceCode = context.getSourceCode(); + + //---------------------------------------------------------------------- + // Public + //---------------------------------------------------------------------- + return { + // VariableDeclaration with kind type has only one VariableDeclarator + "VariableDeclaration[kind='type'] > VariableDeclarator[init.type='TSTypeLiteral']"( + node + ) { + context.report({ + node, + messageId: "interfaceOverType", + fix(fixer) { + const typeNode = node.typeParameters || node.id; + + const fixes = [ + fixer.replaceText( + sourceCode.getFirstToken(node.parent), + "interface" + ), + fixer.replaceTextRange( + [typeNode.range[1], node.init.range[0]], + " " + ), + ]; + + const afterToken = sourceCode.getTokenAfter(node.init); + + if ( + afterToken && + afterToken.type === "Punctuator" && + afterToken.value === ";" + ) { + fixes.push(fixer.remove(afterToken)); + } + + return fixes; + }, + }); + }, + }; + }, +}; diff --git a/tests/lib/rules/prefer-interface.js b/tests/lib/rules/prefer-interface.js new file mode 100644 index 0000000..da54734 --- /dev/null +++ b/tests/lib/rules/prefer-interface.js @@ -0,0 +1,87 @@ +/** + * @fileoverview Prefer an interface declaration over a type literal (type T = { ... }) + * @author Armano + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const rule = require("../../../lib/rules/prefer-interface"), + RuleTester = require("eslint").RuleTester; + +//------------------------------------------------------------------------------ +// Tests +//------------------------------------------------------------------------------ + +const ruleTester = new RuleTester({ + parser: "typescript-eslint-parser", +}); + +ruleTester.run("interface-over-type-literal", rule, { + valid: [ + `var foo = { };`, + `type U = string;`, + `type V = { x: number; } | { y: string; };`, + ` +type Record = { + [K in T]: U; +} + `, + ], + invalid: [ + { + code: `type T = { x: number; }`, + output: `interface T { x: number; }`, + errors: [ + { + messageId: "interfaceOverType", + line: 1, + column: 6, + }, + ], + }, + { + code: `type T={ x: number; }`, + output: `interface T { x: number; }`, + errors: [ + { + messageId: "interfaceOverType", + line: 1, + column: 6, + }, + ], + }, + { + code: `type T= { x: number; }`, + output: `interface T { x: number; }`, + errors: [ + { + messageId: "interfaceOverType", + line: 1, + column: 6, + }, + ], + }, + { + code: ` +export type W = { + x: T, +}; +`, + output: ` +export interface W { + x: T, +} +`, + errors: [ + { + messageId: "interfaceOverType", + line: 2, + column: 13, + }, + ], + }, + ], +});