Skip to content

feat: add allowSeparateTypeImports option to no-duplicate-imports #19872

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jun 20, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
feat: add allowSeparateTypeImports option to no-duplicate-imports
  • Loading branch information
sethamus committed Jun 17, 2025
commit d039005ef152fc134a7a393b6498358f3dd1b911
63 changes: 62 additions & 1 deletion docs/src/rules/no-duplicate-imports.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,12 @@ import * as something from 'module';

## Options

This rule takes one optional argument, an object with a single key, `includeExports` which is a `boolean`. It defaults to `false`.
This rule has an object option:

* `"includeExports"`: `true` (default `false`) checks for exports in addition to imports.
* `"allowSeparateTypeImports"`: `true` (default `false`) allows a type import alongside a value import from the same module in TypeScript files.

### includeExports

If re-exporting from an imported module, you should add the imports to the `import`-statement, and export that directly, not use `export ... from`.

Expand Down Expand Up @@ -110,3 +115,59 @@ export * from 'module';
```

:::

### allowSeparateTypeImports

TypeScript allows specifying a `type` keyword on imports to indicate that the export exists only in the type system, not at runtime. This allows transpilers to drop imports without knowing the types of the dependencies.

Example of **incorrect** TypeScript code for this rule with the default `{ "allowSeparateTypeImports": false }` option:

::: incorrect

```ts
/*eslint no-duplicate-imports: ["error", { "allowSeparateTypeImports": false }]*/

import { someValue } from 'module';
import type { SomeType } from 'module';
```

:::

Example of **correct** TypeScript code for this rule with the default `{ "allowSeparateTypeImports": false }` option:

::: incorrect

```ts
/*eslint no-duplicate-imports: ["error", { "allowSeparateTypeImports": false }]*/

import { someValue, type SomeType } from 'module';
```

:::

Example of **incorrect** TypeScript code for this rule with the `{ "allowSeparateTypeImports": true }` option:

::: incorrect

```ts
/*eslint no-duplicate-imports: ["error", { "allowSeparateTypeImports": true }]*/

import { someValue } from 'module';
import type { SomeType } from 'module';
import type { AnotherType } from 'module';
```

:::

Example of **correct** TypeScript code for this rule with the `{ "allowSeparateTypeImports": true }` option:

::: correct

```ts
/*eslint no-duplicate-imports: ["error", { "allowSeparateTypeImports": true }]*/

import { someValue } from 'module';
import type { SomeType, AnotherType } from 'module';
```

:::
72 changes: 65 additions & 7 deletions lib/rules/no-duplicate-imports.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,13 +91,33 @@ function isImportExportCanBeMerged(node1, node2) {
* Returns a boolean if we should report (import|export).
* @param {ASTNode} node A node to be reported or not.
* @param {[ASTNode]} previousNodes An array contains previous nodes of the module imported or exported.
* @param {boolean} allowSeparateTypeImports Whether to allow separate type and value imports.
* @returns {boolean} True if the (import|export) should be reported.
*/
function shouldReportImportExport(node, previousNodes) {
function shouldReportImportExport(
node,
previousNodes,
allowSeparateTypeImports,
) {
let i = 0;

while (i < previousNodes.length) {
if (isImportExportCanBeMerged(node, previousNodes[i])) {
const previousNode = previousNodes[i];

if (allowSeparateTypeImports) {
const isTypeNode =
node.importKind === "type" || node.exportKind === "type";
const isTypePrevious =
previousNode.importKind === "type" ||
previousNode.exportKind === "type";

if (isTypeNode !== isTypePrevious) {
i++;
continue;
}
}

if (isImportExportCanBeMerged(node, previousNode)) {
return true;
}
i++;
Expand Down Expand Up @@ -136,6 +156,7 @@ function getModule(node) {
* @param {Map} modules A Map object contains as a key a module name and as value an array contains objects, each object contains a node and a declaration type.
* @param {string} declarationType A declaration type can be an import or export.
* @param {boolean} includeExports Whether or not to check for exports in addition to imports.
* @param {boolean} allowSeparateTypeImports Whether to allow separate type and value imports.
* @returns {void} No return value.
*/
function checkAndReport(
Expand All @@ -144,6 +165,7 @@ function checkAndReport(
modules,
declarationType,
includeExports,
allowSeparateTypeImports,
) {
const module = getModule(node);

Expand All @@ -157,19 +179,43 @@ function checkAndReport(
exportNodes = getNodesByDeclarationType(previousNodes, "export");
}
if (declarationType === "import") {
if (shouldReportImportExport(node, importNodes)) {
if (
shouldReportImportExport(
node,
importNodes,
allowSeparateTypeImports,
)
) {
messagesIds.push("import");
}
if (includeExports) {
if (shouldReportImportExport(node, exportNodes)) {
if (
shouldReportImportExport(
node,
exportNodes,
allowSeparateTypeImports,
)
) {
messagesIds.push("importAs");
}
}
} else if (declarationType === "export") {
if (shouldReportImportExport(node, exportNodes)) {
if (
shouldReportImportExport(
node,
exportNodes,
allowSeparateTypeImports,
)
) {
messagesIds.push("export");
}
if (shouldReportImportExport(node, importNodes)) {
if (
shouldReportImportExport(
node,
importNodes,
allowSeparateTypeImports,
)
) {
messagesIds.push("exportAs");
}
}
Expand All @@ -196,13 +242,15 @@ function checkAndReport(
* @param {Map} modules A Map object contains as a key a module name and as value an array contains objects, each object contains a node and a declaration type.
* @param {string} declarationType A declaration type can be an import or export.
* @param {boolean} includeExports Whether or not to check for exports in addition to imports.
* @param {boolean} allowSeparateTypeImports Whether to allow separate type and value imports.
* @returns {nodeCallback} A function passed to ESLint to handle the statement.
*/
function handleImportsExports(
context,
modules,
declarationType,
includeExports,
allowSeparateTypeImports,
) {
return function (node) {
const module = getModule(node);
Expand All @@ -214,6 +262,7 @@ function handleImportsExports(
modules,
declarationType,
includeExports,
allowSeparateTypeImports,
);
const currentNode = { node, declarationType };
let nodes = [currentNode];
Expand All @@ -231,11 +280,14 @@ function handleImportsExports(
/** @type {import('../types').Rule.RuleModule} */
module.exports = {
meta: {
dialects: ["javascript", "typescript"],
language: "javascript",
type: "problem",

defaultOptions: [
{
includeExports: false,
allowSeparateTypeImports: false,
},
],

Expand All @@ -252,6 +304,9 @@ module.exports = {
includeExports: {
type: "boolean",
},
allowSeparateTypeImports: {
type: "boolean",
},
},
additionalProperties: false,
},
Expand All @@ -266,14 +321,15 @@ module.exports = {
},

create(context) {
const [{ includeExports }] = context.options;
const [{ includeExports, allowSeparateTypeImports }] = context.options;
const modules = new Map();
const handlers = {
ImportDeclaration: handleImportsExports(
context,
modules,
"import",
includeExports,
allowSeparateTypeImports,
),
};

Expand All @@ -283,12 +339,14 @@ module.exports = {
modules,
"export",
includeExports,
allowSeparateTypeImports,
);
handlers.ExportAllDeclaration = handleImportsExports(
context,
modules,
"export",
includeExports,
allowSeparateTypeImports,
);
}
return handlers;
Expand Down
4 changes: 4 additions & 0 deletions lib/types/rules.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2288,6 +2288,10 @@ export interface ESLintRules extends Linter.RulesRecord {
* @default false
*/
includeExports: boolean;
/**
* @default false
*/
allowSeparateTypeImports: boolean;
}>,
]
>;
Expand Down
Loading
Loading
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy