Repository: import-js/eslint-plugin-import (js)

Issue:
New rule: consistent inline type specifier usage
In both flow and TS you can mark an import as a type (or typeof for flow) via a specifier.
Both languages support two forms:

top-level, which marks all names in the import as type-only:
```ts
import type Foo from 'Foo';
import type {Bar} from 'Bar';
// ts only 
import type * as Bam from 'Bam';
// flow only
import typeof Baz from 'Baz';
```

inline, which marks just one name in the import as type-only:
```ts
import {type Foo} from 'Foo';
// flow only
import {typeof Bar} from 'Bar';
```

Users care about consistency when it comes to the use of inline type imports - they either want them, or they don't.
I would like to propose a rule which enforces either for, or against the use of inline type specifiers.

The rule would have an option which would enforce one way or another, i.e.
```ts
type Options = [
  'perfer-inline' | 'prefer-top-level',
];
```

## Examples

### ✅ Valid always

```ts
import type Foo from 'Foo'; // default specifiers are always good
import type * as NS from 'Bar'; // namespace specifiers are always good
```

### ✅ Valid with `prefer-top-level`, but ❌ Invalid with `prefer-inline`

```ts
import type {Foo} from 'Foo';
import type Foo, {Bar} from 'Foo';
// flow only
import typeof {Foo} from 'Foo';
```

### ❌ Invalid with `prefer-top-level`, but ✅ Valid with `prefer-inline`

```ts
import {type Foo} from 'Foo';
import Foo, {type Bar} from 'Foo';
// flow only
import {typeof Foo} from 'Foo';
```

## Fixer

For `prefer-top-level`:
```ts
import {Foo, type Bar, typeof Baz} from 'Foo';
// becomes
import {Foo} from 'Foo';
import type {Bar} from 'Bar';
import typeof {Baz} from 'Baz';
```

For `prefer-inline`:
```ts
import type {Foo, Bar} from 'Foo';
// becomes
import {type Foo, type Bar} from 'Foo';

import type Foo, {Bar} from 'Foo';
// becomes
import type Foo from 'Foo';
import {type Bar} from 'Foo';
```

Note that the fixer need not necessarily concern itself with de-duplicating the imports, as `import/no-duplicates` can be turned on by the user to handle that concern.


Pull request description:
[New] `consistent-type-specifier-style`: add rule
Fixes #2469

Adds a new rule as specced in #2469
To test this I had to bump the TS version to 4.5, as that was the first version that supported inline type specifiers.


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 89ed22ac..22a5c386 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -15,6 +15,7 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange
 - [`no-extraneous-dependencies`]: Add `includeInternal` option ([#2541], thanks [@bdwain])
 - [`no-extraneous-dependencies`]: Add `includeTypes` option ([#2543], thanks [@bdwain])
 - [`order`]: new `alphabetize.orderImportKind` option to sort imports with same path based on their kind (`type`, `typeof`) ([#2544], thanks [@stropho])
+- [`consistent-type-specifier-style`]: add rule ([#2473], thanks [@bradzacher])
 
 ### Fixed
 - [`order`]: move nested imports closer to main import entry ([#2396], thanks [@pri1311])
@@ -965,6 +966,7 @@ for info on changes for earlier releases.
 [`import/external-module-folders` setting]: ./README.md#importexternal-module-folders
 [`internal-regex` setting]: ./README.md#importinternal-regex
 
+[`consistent-type-specifier-style`]: ./docs/rules/consistent-type-specifier-style.md
 [`default`]: ./docs/rules/default.md
 [`dynamic-import-chunkname`]: ./docs/rules/dynamic-import-chunkname.md
 [`export`]: ./docs/rules/export.md
@@ -1017,6 +1019,7 @@ for info on changes for earlier releases.
 [#2506]: https://github.com/import-js/eslint-plugin-import/pull/2506
 [#2503]: https://github.com/import-js/eslint-plugin-import/pull/2503
 [#2490]: https://github.com/import-js/eslint-plugin-import/pull/2490
+[#2473]: https://github.com/import-js/eslint-plugin-import/pull/2473
 [#2466]: https://github.com/import-js/eslint-plugin-import/pull/2466
 [#2440]: https://github.com/import-js/eslint-plugin-import/pull/2440
 [#2438]: https://github.com/import-js/eslint-plugin-import/pull/2438
diff --git a/README.md b/README.md
index 329400d5..775fc198 100644
--- a/README.md
+++ b/README.md
@@ -97,6 +97,7 @@ This plugin intends to support linting of ES2015+ (ES6+) import/export syntax, a
 * Forbid anonymous values as default exports ([`no-anonymous-default-export`])
 * Prefer named exports to be grouped together in a single export declaration ([`group-exports`])
 * Enforce a leading comment with the webpackChunkName for dynamic imports ([`dynamic-import-chunkname`])
+* Enforce or ban the use of inline type-only markers for named imports ([`consistent-type-specifier-style`])
 
 [`first`]: ./docs/rules/first.md
 [`exports-last`]: ./docs/rules/exports-last.md
@@ -114,6 +115,7 @@ This plugin intends to support linting of ES2015+ (ES6+) import/export syntax, a
 [`no-default-export`]: ./docs/rules/no-default-export.md
 [`no-named-export`]: ./docs/rules/no-named-export.md
 [`dynamic-import-chunkname`]: ./docs/rules/dynamic-import-chunkname.md
+[`consistent-type-specifier-style`]: ./docs/rules/consistent-type-specifier-style.md
 
 ## `eslint-plugin-import` for enterprise
 
diff --git a/docs/rules/consistent-type-specifier-style.md b/docs/rules/consistent-type-specifier-style.md
new file mode 100644
index 00000000..4314e338
--- /dev/null
+++ b/docs/rules/consistent-type-specifier-style.md
@@ -0,0 +1,87 @@
+# import/consistent-type-specifier-style
+
+In both Flow and TypeScript you can mark an import as a type-only import by adding a "kind" marker to the import. Both languages support two positions for marker.
+
+**At the top-level** which marks all names in the import as type-only and applies to named, default, and namespace (for TypeScript) specifiers:
+
+```ts
+import type Foo from 'Foo';
+import type {Bar} from 'Bar';
+// ts only
+import type * as Bam from 'Bam';
+// flow only
+import typeof Baz from 'Baz';
+```
+
+**Inline** with to the named import, which marks just the specific name in the import as type-only. An inline specifier is only valid for named specifiers, and not for default or namespace specifiers:
+
+```ts
+import {type Foo} from 'Foo';
+// flow only
+import {typeof Bar} from 'Bar';
+```
+
+## Rule Details
+
+This rule either enforces or bans the use of inline type-only markers for named imports.
+
+This rule includes a fixer that will automatically convert your specifiers to the correct form - however the fixer will not respect your preferences around de-duplicating imports. If this is important to you, consider using the [`import/no-duplicates`] rule.
+
+[`import/no-duplicates`]: ./no-duplicates.md
+
+## Options
+
+The rule accepts a single string option which may be one of:
+
+- `'prefer-inline'` - enforces that named type-only specifiers are only ever written with an inline marker; and never as part of a top-level, type-only import.
+- `'prefer-top-level'` - enforces that named type-only specifiers only ever written as part of a top-level, type-only import; and never with an inline marker.
+
+By default the rule will use the `prefer-inline` option.
+
+## Examples
+
+### `prefer-top-level`
+
+❌ Invalid with `["error", "prefer-top-level"]`
+
+```ts
+import {type Foo} from 'Foo';
+import Foo, {type Bar} from 'Foo';
+// flow only
+import {typeof Foo} from 'Foo';
+```
+
+✅ Valid with `["error", "prefer-top-level"]`
+
+```ts
+import type {Foo} from 'Foo';
+import type Foo, {Bar} from 'Foo';
+// flow only
+import typeof {Foo} from 'Foo';
+```
+
+### `prefer-inline`
+
+❌ Invalid with `["error", "prefer-inline"]`
+
+```ts
+import type {Foo} from 'Foo';
+import type Foo, {Bar} from 'Foo';
+// flow only
+import typeof {Foo} from 'Foo';
+```
+
+✅ Valid with `["error", "prefer-inline"]`
+
+```ts
+import {type Foo} from 'Foo';
+import Foo, {type Bar} from 'Foo';
+// flow only
+import {typeof Foo} from 'Foo';
+```
+
+## When Not To Use It
+
+If you aren't using Flow or TypeScript 4.5+, then this rule does not apply and need not be used.
+
+If you don't care about, and don't want to standardize how named specifiers are imported then you should not use this rule.
diff --git a/package.json b/package.json
index ed873323..c3ffef58 100644
--- a/package.json
+++ b/package.json
@@ -92,7 +92,7 @@
     "safe-publish-latest": "^2.0.0",
     "semver": "^6.3.0",
     "sinon": "^2.4.1",
-    "typescript": "^2.8.1 || ~3.9.5",
+    "typescript": "^2.8.1 || ~3.9.5 || ~4.5.2",
     "typescript-eslint-parser": "^15 || ^20 || ^22"
   },
   "peerDependencies": {
diff --git a/src/index.js b/src/index.js
index 7fa3710d..fd83a4aa 100644
--- a/src/index.js
+++ b/src/index.js
@@ -12,6 +12,7 @@ export const rules = {
   'group-exports': require('./rules/group-exports'),
   'no-relative-packages': require('./rules/no-relative-packages'),
   'no-relative-parent-imports': require('./rules/no-relative-parent-imports'),
+  'consistent-type-specifier-style': require('./rules/consistent-type-specifier-style'),
 
   'no-self-import': require('./rules/no-self-import'),
   'no-cycle': require('./rules/no-cycle'),
diff --git a/src/rules/consistent-type-specifier-style.js b/src/rules/consistent-type-specifier-style.js
new file mode 100644
index 00000000..73e0ba92
--- /dev/null
+++ b/src/rules/consistent-type-specifier-style.js
@@ -0,0 +1,216 @@
+import docsUrl from '../docsUrl';
+
+function isComma(token) {
+  return token.type === 'Punctuator' && token.value === ',';
+}
+
+function removeSpecifiers(fixes, fixer, sourceCode, specifiers) {
+  for (const specifier of specifiers) {
+    // remove the trailing comma
+    const comma = sourceCode.getTokenAfter(specifier, isComma);
+    if (comma) {
+      fixes.push(fixer.remove(comma));
+    }
+    fixes.push(fixer.remove(specifier));
+  }
+}
+
+function getImportText(
+  node,
+  sourceCode,
+  specifiers,
+  kind,
+) {
+  const sourceString = sourceCode.getText(node.source);
+  if (specifiers.length === 0) {
+    return '';
+  }
+
+  const names = specifiers.map(s => {
+    if (s.imported.name === s.local.name) {
+      return s.imported.name;
+    }
+    return `${s.imported.name} as ${s.local.name}`;
+  });
+  // insert a fresh top-level import
+  return `import ${kind} {${names.join(', ')}} from ${sourceString};`;
+}
+
+module.exports = {
+  meta: {
+    type: 'suggestion',
+    docs: {
+      description: 'Enforce or ban the use of inline type-only markers for named imports',
+      url: docsUrl('consistent-type-specifier-style'),
+    },
+    fixable: 'code',
+    schema: [
+      {
+        type: 'string',
+        enum: ['prefer-inline', 'prefer-top-level'],
+        default: 'prefer-inline',
+      },
+    ],
+  },
+
+  create(context) {
+    const sourceCode = context.getSourceCode();
+
+    if (context.options[0] === 'prefer-inline') {
+      return {
+        ImportDeclaration(node) {
+          if (node.importKind === 'value' || node.importKind == null) {
+            // top-level value / unknown is valid
+            return;
+          }
+
+          if (
+            // no specifiers (import type {} from '') have no specifiers to mark as inline
+            node.specifiers.length === 0 ||
+            (node.specifiers.length === 1 &&
+              // default imports are both "inline" and "top-level"
+              (node.specifiers[0].type === 'ImportDefaultSpecifier' ||
+                // namespace imports are both "inline" and "top-level"
+                node.specifiers[0].type === 'ImportNamespaceSpecifier'))
+          ) {
+            return;
+          }
+
+          context.report({
+            node,
+            message: 'Prefer using inline {{kind}} specifiers instead of a top-level {{kind}}-only import.',
+            data: {
+              kind: node.importKind,
+            },
+            fix(fixer) {
+              const kindToken = sourceCode.getFirstToken(node, { skip: 1 });
+
+              return [].concat(
+                kindToken ? fixer.remove(kindToken) : [],
+                node.specifiers.map((specifier) => fixer.insertTextBefore(specifier, `${node.importKind} `)),
+              );
+            },
+          });
+        },
+      };
+    }
+
+    // prefer-top-level
+    return {
+      ImportDeclaration(node) {
+        if (
+          // already top-level is valid
+          node.importKind === 'type' ||
+          node.importKind === 'typeof' ||
+          // no specifiers (import {} from '') cannot have inline - so is valid
+          node.specifiers.length === 0 ||
+          (node.specifiers.length === 1 &&
+            // default imports are both "inline" and "top-level"
+            (node.specifiers[0].type === 'ImportDefaultSpecifier' ||
+              // namespace imports are both "inline" and "top-level"
+              node.specifiers[0].type === 'ImportNamespaceSpecifier'))
+        ) {
+          return;
+        }
+
+        const typeSpecifiers = [];
+        const typeofSpecifiers = [];
+        const valueSpecifiers = [];
+        let defaultSpecifier = null;
+        for (const specifier of node.specifiers) {
+          if (specifier.type === 'ImportDefaultSpecifier') {
+            defaultSpecifier = specifier;
+            continue;
+          }
+
+          if (specifier.importKind === 'type') {
+            typeSpecifiers.push(specifier);
+          } else if (specifier.importKind === 'typeof') {
+            typeofSpecifiers.push(specifier);
+          } else if (specifier.importKind === 'value' || specifier.importKind == null) {
+            valueSpecifiers.push(specifier);
+          }
+        }
+
+        const typeImport = getImportText(node, sourceCode, typeSpecifiers, 'type');
+        const typeofImport = getImportText(node, sourceCode, typeofSpecifiers, 'typeof');
+        const newImports = `${typeImport}\n${typeofImport}`.trim();
+
+        if (typeSpecifiers.length + typeofSpecifiers.length === node.specifiers.length) {
+          // all specifiers have inline specifiers - so we replace the entire import
+          const kind = [].concat(
+            typeSpecifiers.length > 0 ? 'type' : [],
+            typeofSpecifiers.length > 0 ? 'typeof' : [],
+          );
+
+          context.report({
+            node,
+            message: 'Prefer using a top-level {{kind}}-only import instead of inline {{kind}} specifiers.',
+            data: {
+              kind: kind.join('/'),
+            },
+            fix(fixer) {
+              return fixer.replaceText(node, newImports);
+            },
+          });
+        } else {
+          // remove specific specifiers and insert new imports for them
+          for (const specifier of typeSpecifiers.concat(typeofSpecifiers)) {
+            context.report({
+              node: specifier,
+              message: 'Prefer using a top-level {{kind}}-only import instead of inline {{kind}} specifiers.',
+              data: {
+                kind: specifier.importKind,
+              },
+              fix(fixer) {
+                const fixes = [];
+
+                // if there are no value specifiers, then the other report fixer will be called, not this one
+
+                if (valueSpecifiers.length > 0) {
+                  // import { Value, type Type } from 'mod';
+
+                  // we can just remove the type specifiers
+                  removeSpecifiers(fixes, fixer, sourceCode, typeSpecifiers);
+                  removeSpecifiers(fixes, fixer, sourceCode, typeofSpecifiers);
+
+                  // make the import nicely formatted by also removing the trailing comma after the last value import
+                  // eg
+                  // import { Value, type Type } from 'mod';
+                  // to
+                  // import { Value  } from 'mod';
+                  // not
+                  // import { Value,  } from 'mod';
+                  const maybeComma = sourceCode.getTokenAfter(valueSpecifiers[valueSpecifiers.length - 1]);
+                  if (isComma(maybeComma)) {
+                    fixes.push(fixer.remove(maybeComma));
+                  }
+                } else if (defaultSpecifier) {
+                  // import Default, { type Type } from 'mod';
+
+                  // remove the entire curly block so we don't leave an empty one behind
+                  // NOTE - the default specifier *must* be the first specifier always!
+                  //        so a comma exists that we also have to clean up or else it's bad syntax
+                  const comma = sourceCode.getTokenAfter(defaultSpecifier, isComma);
+                  const closingBrace = sourceCode.getTokenAfter(
+                    node.specifiers[node.specifiers.length - 1],
+                    token => token.type === 'Punctuator' && token.value === '}',
+                  );
+                  fixes.push(fixer.removeRange([
+                    comma.range[0],
+                    closingBrace.range[1],
+                  ]));
+                }
+
+                return fixes.concat(
+                  // insert the new imports after the old declaration
+                  fixer.insertTextAfter(node, `\n${newImports}`),
+                );
+              },
+            });
+          }
+        }
+      },
+    };
+  },
+};


Test patch:
diff --git a/tests/src/core/getExports.js b/tests/src/core/getExports.js
index dcfa74d8..6dea6e02 100644
--- a/tests/src/core/getExports.js
+++ b/tests/src/core/getExports.js
@@ -2,6 +2,7 @@ import { expect } from  'chai';
 import semver from 'semver';
 import sinon from 'sinon';
 import eslintPkg from 'eslint/package.json';
+import typescriptPkg from 'typescript/package.json';
 import * as tsConfigLoader from 'tsconfig-paths/lib/tsconfig-loader';
 import ExportMap from '../../../src/ExportMap';
 
@@ -351,7 +352,7 @@ describe('ExportMap', function () {
       configs.push(['array form', { '@typescript-eslint/parser': ['.ts', '.tsx'] }]);
     }
 
-    if (semver.satisfies(eslintPkg.version, '<6')) {
+    if (semver.satisfies(eslintPkg.version, '<6') && semver.satisfies(typescriptPkg.version, '<4')) {
       configs.push(['array form', { 'typescript-eslint-parser': ['.ts', '.tsx'] }]);
     }
 
diff --git a/tests/src/rules/consistent-type-specifier-style.js b/tests/src/rules/consistent-type-specifier-style.js
new file mode 100644
index 00000000..31a4c09f
--- /dev/null
+++ b/tests/src/rules/consistent-type-specifier-style.js
@@ -0,0 +1,403 @@
+import { RuleTester } from 'eslint';
+import { test, parsers, tsVersionSatisfies, eslintVersionSatisfies, typescriptEslintParserSatisfies } from '../utils';
+
+const rule = require('rules/consistent-type-specifier-style');
+
+const COMMON_TESTS = {
+  valid: [
+    //
+    // prefer-top-level
+    //
+    test({
+      code: "import Foo from 'Foo';",
+      options: ['prefer-top-level'],
+    }),
+    test({
+      code: "import type Foo from 'Foo';",
+      options: ['prefer-top-level'],
+    }),
+    test({
+      code: "import { Foo } from 'Foo';",
+      options: ['prefer-top-level'],
+    }),
+    test({
+      code: "import { Foo as Bar } from 'Foo';",
+      options: ['prefer-top-level'],
+    }),
+    test({
+      code: "import * as Foo from 'Foo';",
+      options: ['prefer-top-level'],
+    }),
+    test({
+      code: "import 'Foo';",
+      options: ['prefer-top-level'],
+    }),
+    test({
+      code: "import {} from 'Foo';",
+      options: ['prefer-top-level'],
+    }),
+    test({
+      code: "import type {} from 'Foo';",
+      options: ['prefer-top-level'],
+    }),
+    test({
+      code: "import type { Foo } from 'Foo';",
+      options: ['prefer-top-level'],
+    }),
+    test({
+      code: "import type { Foo as Bar } from 'Foo';",
+      options: ['prefer-top-level'],
+    }),
+    test({
+      code: "import type { Foo, Bar, Baz, Bam } from 'Foo';",
+      options: ['prefer-top-level'],
+    }),
+
+    //
+    // prefer-inline
+    //
+    test({
+      code: "import Foo from 'Foo';",
+      options: ['prefer-inline'],
+    }),
+    test({
+      code: "import type Foo from 'Foo';",
+      options: ['prefer-inline'],
+    }),
+    test({
+      code: "import { Foo } from 'Foo';",
+      options: ['prefer-inline'],
+    }),
+    test({
+      code: "import { Foo as Bar } from 'Foo';",
+      options: ['prefer-inline'],
+    }),
+    test({
+      code: "import * as Foo from 'Foo';",
+      options: ['prefer-inline'],
+    }),
+    test({
+      code: "import 'Foo';",
+      options: ['prefer-inline'],
+    }),
+    test({
+      code: "import {} from 'Foo';",
+      options: ['prefer-inline'],
+    }),
+    test({
+      code: "import type {} from 'Foo';",
+      options: ['prefer-inline'],
+    }),
+    test({
+      code: "import { type Foo } from 'Foo';",
+      options: ['prefer-inline'],
+    }),
+    test({
+      code: "import { type Foo as Bar } from 'Foo';",
+      options: ['prefer-inline'],
+    }),
+    test({
+      code: "import { type Foo, type Bar, Baz, Bam } from 'Foo';",
+      options: ['prefer-inline'],
+    }),
+  ],
+  invalid: [
+    //
+    // prefer-top-level
+    //
+    {
+      code: "import { type Foo } from 'Foo';",
+      output: "import type {Foo} from 'Foo';",
+      options: ['prefer-top-level'],
+      errors: [{
+        message: 'Prefer using a top-level type-only import instead of inline type specifiers.',
+        type: 'ImportDeclaration',
+      }],
+    },
+    {
+      code: "import { type Foo as Bar } from 'Foo';",
+      output: "import type {Foo as Bar} from 'Foo';",
+      options: ['prefer-top-level'],
+      errors: [{
+        message: 'Prefer using a top-level type-only import instead of inline type specifiers.',
+        type: 'ImportDeclaration',
+      }],
+    },
+    {
+      code: "import { type Foo, type Bar } from 'Foo';",
+      output: "import type {Foo, Bar} from 'Foo';",
+      options: ['prefer-top-level'],
+      errors: [{
+        message: 'Prefer using a top-level type-only import instead of inline type specifiers.',
+        type: 'ImportDeclaration',
+      }],
+    },
+    {
+      code: "import { Foo, type Bar } from 'Foo';",
+      output: "import { Foo  } from 'Foo';\nimport type {Bar} from 'Foo';",
+      options: ['prefer-top-level'],
+      errors: [{
+        message: 'Prefer using a top-level type-only import instead of inline type specifiers.',
+        type: 'ImportSpecifier',
+      }],
+    },
+    {
+      code: "import { type Foo, Bar } from 'Foo';",
+      output: "import {  Bar } from 'Foo';\nimport type {Foo} from 'Foo';",
+      options: ['prefer-top-level'],
+      errors: [{
+        message: 'Prefer using a top-level type-only import instead of inline type specifiers.',
+        type: 'ImportSpecifier',
+      }],
+    },
+    {
+      code: "import Foo, { type Bar } from 'Foo';",
+      output: "import Foo from 'Foo';\nimport type {Bar} from 'Foo';",
+      options: ['prefer-top-level'],
+      errors: [{
+        message: 'Prefer using a top-level type-only import instead of inline type specifiers.',
+        type: 'ImportSpecifier',
+      }],
+    },
+    {
+      code: "import Foo, { type Bar, Baz } from 'Foo';",
+      output: "import Foo, {  Baz } from 'Foo';\nimport type {Bar} from 'Foo';",
+      options: ['prefer-top-level'],
+      errors: [{
+        message: 'Prefer using a top-level type-only import instead of inline type specifiers.',
+        type: 'ImportSpecifier',
+      }],
+    },
+
+    //
+    // prefer-inline
+    //
+    {
+      code: "import type { Foo } from 'Foo';",
+      output: "import  { type Foo } from 'Foo';",
+      options: ['prefer-inline'],
+      errors: [{
+        message: 'Prefer using inline type specifiers instead of a top-level type-only import.',
+        type: 'ImportDeclaration',
+      }],
+    },
+    {
+      code: "import type { Foo, Bar, Baz } from 'Foo';",
+      output: "import  { type Foo, type Bar, type Baz } from 'Foo';",
+      options: ['prefer-inline'],
+      errors: [{
+        message: 'Prefer using inline type specifiers instead of a top-level type-only import.',
+        type: 'ImportDeclaration',
+      }],
+    },
+  ],
+};
+
+const TS_ONLY = {
+  valid: [
+    //
+    // always valid
+    //
+    test({ code: "import type * as Foo from 'Foo';" }),
+  ],
+  invalid: [],
+};
+
+const FLOW_ONLY = {
+  valid: [
+    //
+    // prefer-top-level
+    //
+    {
+      code: "import typeof Foo from 'Foo';",
+      options: ['prefer-top-level'],
+    },
+    {
+      code: "import typeof { Foo, Bar, Baz, Bam } from 'Foo';",
+      options: ['prefer-top-level'],
+    },
+
+    //
+    // prefer-inline
+    //
+    {
+      code: "import typeof Foo from 'Foo';",
+      options: ['prefer-inline'],
+    },
+    {
+      code: "import { typeof Foo } from 'Foo';",
+      options: ['prefer-inline'],
+    },
+    {
+      code: "import { typeof Foo, typeof Bar, typeof Baz, typeof Bam } from 'Foo';",
+      options: ['prefer-inline'],
+    },
+    {
+      code: "import { type Foo, type Bar, typeof Baz, typeof Bam } from 'Foo';",
+      options: ['prefer-inline'],
+    },
+  ],
+  invalid: [
+    //
+    // prefer-top-level
+    //
+    {
+      code: "import { typeof Foo } from 'Foo';",
+      output: "import typeof {Foo} from 'Foo';",
+      options: ['prefer-top-level'],
+      errors: [{
+        message: 'Prefer using a top-level typeof-only import instead of inline typeof specifiers.',
+        type: 'ImportDeclaration',
+      }],
+    },
+    {
+      code: "import { typeof Foo as Bar } from 'Foo';",
+      output: "import typeof {Foo as Bar} from 'Foo';",
+      options: ['prefer-top-level'],
+      errors: [{
+        message: 'Prefer using a top-level typeof-only import instead of inline typeof specifiers.',
+        type: 'ImportDeclaration',
+      }],
+    },
+    {
+      code: "import { type Foo, typeof Bar } from 'Foo';",
+      output: "import type {Foo} from 'Foo';\nimport typeof {Bar} from 'Foo';",
+      options: ['prefer-top-level'],
+      errors: [{
+        message: 'Prefer using a top-level type/typeof-only import instead of inline type/typeof specifiers.',
+        type: 'ImportDeclaration',
+      }],
+    },
+    {
+      code: "import { typeof Foo, typeof Bar } from 'Foo';",
+      output: "import typeof {Foo, Bar} from 'Foo';",
+      options: ['prefer-top-level'],
+      errors: [{
+        message: 'Prefer using a top-level typeof-only import instead of inline typeof specifiers.',
+        type: 'ImportDeclaration',
+      }],
+    },
+    {
+      code: "import { Foo, typeof Bar } from 'Foo';",
+      output: "import { Foo  } from 'Foo';\nimport typeof {Bar} from 'Foo';",
+      options: ['prefer-top-level'],
+      errors: [{
+        message: 'Prefer using a top-level typeof-only import instead of inline typeof specifiers.',
+        type: 'ImportSpecifier',
+      }],
+    },
+    {
+      code: "import { typeof Foo, Bar } from 'Foo';",
+      output: "import {  Bar } from 'Foo';\nimport typeof {Foo} from 'Foo';",
+      options: ['prefer-top-level'],
+      errors: [{
+        message: 'Prefer using a top-level typeof-only import instead of inline typeof specifiers.',
+        type: 'ImportSpecifier',
+      }],
+    },
+    {
+      code: "import { Foo, type Bar, typeof Baz } from 'Foo';",
+      output: "import { Foo   } from 'Foo';\nimport type {Bar} from 'Foo';\nimport typeof {Baz} from 'Foo';",
+      options: ['prefer-top-level'],
+      errors: [
+        {
+          message: 'Prefer using a top-level type-only import instead of inline type specifiers.',
+          type: 'ImportSpecifier',
+        },
+        {
+          message: 'Prefer using a top-level typeof-only import instead of inline typeof specifiers.',
+          type: 'ImportSpecifier',
+        },
+      ],
+    },
+    {
+      code: "import Foo, { typeof Bar } from 'Foo';",
+      output: "import Foo from 'Foo';\nimport typeof {Bar} from 'Foo';",
+      options: ['prefer-top-level'],
+      errors: [{
+        message: 'Prefer using a top-level typeof-only import instead of inline typeof specifiers.',
+        type: 'ImportSpecifier',
+      }],
+    },
+    {
+      code: "import Foo, { typeof Bar, Baz } from 'Foo';",
+      output: "import Foo, {  Baz } from 'Foo';\nimport typeof {Bar} from 'Foo';",
+      options: ['prefer-top-level'],
+      errors: [{
+        message: 'Prefer using a top-level typeof-only import instead of inline typeof specifiers.',
+        type: 'ImportSpecifier',
+      }],
+    },
+
+    //
+    // prefer-inline
+    //
+    {
+      code: "import typeof { Foo } from 'Foo';",
+      output: "import  { typeof Foo } from 'Foo';",
+      options: ['prefer-inline'],
+      errors: [{
+        message: 'Prefer using inline typeof specifiers instead of a top-level typeof-only import.',
+        type: 'ImportDeclaration',
+      }],
+    },
+    {
+      code: "import typeof { Foo, Bar, Baz } from 'Foo';",
+      output: "import  { typeof Foo, typeof Bar, typeof Baz } from 'Foo';",
+      options: ['prefer-inline'],
+      errors: [{
+        message: 'Prefer using inline typeof specifiers instead of a top-level typeof-only import.',
+        type: 'ImportDeclaration',
+      }],
+    },
+  ],
+};
+
+context('TypeScript', () => {
+  // inline type specifiers weren't supported prior to TS v4.5
+  if (!parsers.TS_NEW || !tsVersionSatisfies('>= 4.5') || !typescriptEslintParserSatisfies('>= 5.7.0')) {
+    return;
+  }
+
+  const ruleTester = new RuleTester({
+    parser: parsers.TS_NEW,
+    parserOptions: {
+      ecmaVersion: 6,
+      sourceType: 'module',
+    },
+  });
+  ruleTester.run('consistent-type-specifier-style', rule, {
+    valid: [
+      ...COMMON_TESTS.valid,
+      ...TS_ONLY.valid,
+    ],
+    invalid: [
+      ...COMMON_TESTS.invalid,
+      ...TS_ONLY.invalid,
+    ],
+  });
+});
+
+context('Babel/Flow', () => {
+  if (!eslintVersionSatisfies('> 3')) {
+    return;
+  }
+
+  const ruleTester = new RuleTester({
+    parser: parsers.BABEL_OLD,
+    parserOptions: {
+      ecmaVersion: 6,
+      sourceType: 'module',
+    },
+  });
+  ruleTester.run('consistent-type-specifier-style', rule, {
+    valid: [
+      ...COMMON_TESTS.valid,
+      ...FLOW_ONLY.valid,
+    ],
+    invalid: [
+      ...COMMON_TESTS.invalid,
+      ...FLOW_ONLY.invalid,
+    ],
+  });
+});
diff --git a/tests/src/utils.js b/tests/src/utils.js
index ed04aa96..b82883a6 100644
--- a/tests/src/utils.js
+++ b/tests/src/utils.js
@@ -1,17 +1,26 @@
 import path from 'path';
 import eslintPkg from 'eslint/package.json';
 import semver from 'semver';
+import typescriptPkg from 'typescript/package.json';
 
 // warms up the module cache. this import takes a while (>500ms)
 import 'babel-eslint';
 
 export const parsers = {
   ESPREE: require.resolve('espree'),
-  TS_OLD: semver.satisfies(eslintPkg.version, '>=4.0.0 <6.0.0') && require.resolve('typescript-eslint-parser'),
+  TS_OLD: semver.satisfies(eslintPkg.version, '>=4.0.0 <6.0.0') && semver.satisfies(typescriptPkg.version, '<4') && require.resolve('typescript-eslint-parser'),
   TS_NEW: semver.satisfies(eslintPkg.version, '> 5') && require.resolve('@typescript-eslint/parser'),
   BABEL_OLD: require.resolve('babel-eslint'),
 };
 
+export function tsVersionSatisfies(specifier) {
+  return semver.satisfies(typescriptPkg.version, specifier);
+}
+
+export function typescriptEslintParserSatisfies(specifier) {
+  return parsers.TS_NEW && semver.satisfies(require('@typescript-eslint/parser/package.json').version, specifier);
+}
+
 export function testFilePath(relativePath) {
   return path.join(process.cwd(), './tests/files', relativePath);
 }
@@ -29,8 +38,12 @@ export function getNonDefaultParsers() {
 
 export const FILENAME = testFilePath('foo.js');
 
+export function eslintVersionSatisfies(specifier) {
+  return semver.satisfies(eslintPkg.version, specifier);
+}
+
 export function testVersion(specifier, t) {
-  return semver.satisfies(eslintPkg.version, specifier) ? test(t()) : [];
+  return eslintVersionSatisfies(specifier) ? test(t()) : [];
 }
 
 export function test(t) {