Repository: import-js/eslint-plugin-import
Pull request #2817 (2023-07-05 10:21:11)

[New] `extensions`: add the `checkTypeImports` option
Currently, the patterns 
```ts
import type { Foo } from './foo';

export type { Foo } from './foo';
```

are ignored by the `import/extensions` rule.

This adds the `checkTypeImports` option to that rule, so these types of imports can be checked on an opt-in basis.


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 795fa5bc..d1dc35e7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,7 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange
 ### Added
 - support eslint v9 ([#2996], thanks [@G-Rath] [@michaelfaith])
 - [`order`]: allow validating named imports ([#3043], thanks [@manuth])
+- [`extensions`]: add the `checkTypeImports` option ([#2817], thanks [@phryneas])
 
 ### Fixed
 - `ExportMap` / flat config: include `languageOptions` in context ([#3052], thanks [@michaelfaith])
@@ -1187,6 +1188,7 @@ for info on changes for earlier releases.
 [#2842]: https://github.com/import-js/eslint-plugin-import/pull/2842
 [#2835]: https://github.com/import-js/eslint-plugin-import/pull/2835
 [#2832]: https://github.com/import-js/eslint-plugin-import/pull/2832
+[#2817]: https://github.com/import-js/eslint-plugin-import/pull/2817
 [#2778]: https://github.com/import-js/eslint-plugin-import/pull/2778
 [#2756]: https://github.com/import-js/eslint-plugin-import/pull/2756
 [#2754]: https://github.com/import-js/eslint-plugin-import/pull/2754
@@ -1942,6 +1944,7 @@ for info on changes for earlier releases.
 [@pcorpet]: https://github.com/pcorpet
 [@Pearce-Ropion]: https://github.com/Pearce-Ropion
 [@Pessimistress]: https://github.com/Pessimistress
+[@phryneas]: https://github.com/phryneas
 [@pmcelhaney]: https://github.com/pmcelhaney
 [@preco21]: https://github.com/preco21
 [@pri1311]: https://github.com/pri1311
diff --git a/docs/rules/extensions.md b/docs/rules/extensions.md
index 946ccb7b..5d15e93f 100644
--- a/docs/rules/extensions.md
+++ b/docs/rules/extensions.md
@@ -56,6 +56,8 @@ For example, `["error", "never", { "svg": "always" }]` would require that all ex
 In that case, if you still want to specify extensions, you can do so inside the **pattern** property.
 Default value of `ignorePackages` is `false`.
 
+By default, `import type` and `export type` style imports/exports are ignored. If you want to check them as well, you can set the `checkTypeImports` option to `true`.
+
 ### Exception
 
 When disallowing the use of certain extensions this rule makes an exception and allows the use of extension when the file would not be resolvable without extension.
@@ -104,6 +106,14 @@ import express from 'express/index';
 import * as path from 'path';
 ```
 
+The following patterns are considered problems when the configuration is set to "never" and the option "checkTypeImports" is set to `true`:
+
+```js
+import type { Foo } from './foo.ts';
+
+export type { Foo } from './foo.ts';
+```
+
 The following patterns are considered problems when configuration set to "always":
 
 ```js
@@ -167,6 +177,14 @@ import express from 'express';
 import foo from '@/foo';
 ```
 
+The following patterns are considered problems when the configuration is set to "always" and the option "checkTypeImports" is set to `true`:
+
+```js
+import type { Foo } from './foo';
+
+export type { Foo } from './foo';
+```
+
 ## When Not To Use It
 
 If you are not concerned about a consistent usage of file extension.
diff --git a/src/rules/extensions.js b/src/rules/extensions.js
index b1e5c6d9..c2c03a2b 100644
--- a/src/rules/extensions.js
+++ b/src/rules/extensions.js
@@ -14,6 +14,7 @@ const properties = {
   type: 'object',
   properties: {
     pattern: patternProperties,
+    checkTypeImports: { type: 'boolean' },
     ignorePackages: { type: 'boolean' },
   },
 };
@@ -35,7 +36,7 @@ function buildProperties(context) {
     }
 
     // If this is not the new structure, transfer all props to result.pattern
-    if (obj.pattern === undefined && obj.ignorePackages === undefined) {
+    if (obj.pattern === undefined && obj.ignorePackages === undefined && obj.checkTypeImports === undefined) {
       Object.assign(result.pattern, obj);
       return;
     }
@@ -49,6 +50,10 @@ function buildProperties(context) {
     if (obj.ignorePackages !== undefined) {
       result.ignorePackages = obj.ignorePackages;
     }
+
+    if (obj.checkTypeImports !== undefined) {
+      result.checkTypeImports = obj.checkTypeImports;
+    }
   });
 
   if (result.defaultConfig === 'ignorePackages') {
@@ -168,7 +173,7 @@ module.exports = {
 
       if (!extension || !importPath.endsWith(`.${extension}`)) {
         // ignore type-only imports and exports
-        if (node.importKind === 'type' || node.exportKind === 'type') { return; }
+        if (!props.checkTypeImports && (node.importKind === 'type' || node.exportKind === 'type')) { return; }
         const extensionRequired = isUseOfExtensionRequired(extension, isPackage);
         const extensionForbidden = isUseOfExtensionForbidden(extension);
         if (extensionRequired && !extensionForbidden) {


Test patch:
diff --git a/tests/src/rules/extensions.js b/tests/src/rules/extensions.js
index 97267832..883dfab6 100644
--- a/tests/src/rules/extensions.js
+++ b/tests/src/rules/extensions.js
@@ -3,6 +3,15 @@ import rule from 'rules/extensions';
 import { getTSParsers, test, testFilePath, parsers } from '../utils';
 
 const ruleTester = new RuleTester();
+const ruleTesterWithTypeScriptImports = new RuleTester({
+  settings: {
+    'import/resolver': {
+      typescript: {
+        alwaysTryTypes: true,
+      },
+    },
+  },
+});
 
 ruleTester.run('extensions', rule, {
   valid: [
@@ -689,6 +698,64 @@ describe('TypeScript', () => {
             ],
             parser,
           }),
+          test({
+            code: 'import type T from "./typescript-declare";',
+            errors: ['Missing file extension for "./typescript-declare"'],
+            options: [
+              'always',
+              { ts: 'never', tsx: 'never', js: 'never', jsx: 'never', checkTypeImports: true },
+            ],
+            parser,
+          }),
+          test({
+            code: 'export type { MyType } from "./typescript-declare";',
+            errors: ['Missing file extension for "./typescript-declare"'],
+            options: [
+              'always',
+              { ts: 'never', tsx: 'never', js: 'never', jsx: 'never', checkTypeImports: true },
+            ],
+            parser,
+          }),
+        ],
+      });
+      ruleTesterWithTypeScriptImports.run(`${parser}: (with TS resolver) extensions are enforced for type imports/export when checkTypeImports is set`, rule, {
+        valid: [
+          test({
+            code: 'import type { MyType } from "./typescript-declare.ts";',
+            options: [
+              'always',
+              { checkTypeImports: true },
+            ],
+            parser,
+          }),
+          test({
+            code: 'export type { MyType } from "./typescript-declare.ts";',
+            options: [
+              'always',
+              { checkTypeImports: true },
+            ],
+            parser,
+          }),
+        ],
+        invalid: [
+          test({
+            code: 'import type { MyType } from "./typescript-declare";',
+            errors: ['Missing file extension "ts" for "./typescript-declare"'],
+            options: [
+              'always',
+              { checkTypeImports: true },
+            ],
+            parser,
+          }),
+          test({
+            code: 'export type { MyType } from "./typescript-declare";',
+            errors: ['Missing file extension "ts" for "./typescript-declare"'],
+            options: [
+              'always',
+              { checkTypeImports: true },
+            ],
+            parser,
+          }),
         ],
       });
     });