Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
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
3 changes: 2 additions & 1 deletion packages/comparisons/src/data.json
Original file line number Diff line number Diff line change
Expand Up @@ -17357,7 +17357,8 @@
"flint": {
"name": "typeExports",
"plugin": "ts",
"preset": "stylistic"
"preset": "stylistic",
"status": "implemented"
}
},
{
Expand Down
74 changes: 74 additions & 0 deletions packages/site/src/content/docs/rules/ts/typeExports.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
---
description: "Reports exports that should use 'export type' syntax."
title: "typeExports"
topic: "rules"
---

import { Tabs, TabItem } from "@astrojs/starlight/components";
import { RuleEquivalents } from "~/components/RuleEquivalents";
import RuleSummary from "~/components/RuleSummary.astro";

<RuleSummary plugin="ts" rule="typeExports" />

Using `export type` for type-only exports improves tree-shaking and makes the intent explicit.
When exporting symbols that were imported using `import type`, use `export type` instead.

## Examples

### Type-Only Re-exports

<Tabs>
<TabItem label="❌ Incorrect">

```ts
import type { User } from "./types";
export { User };
```

</TabItem>
<TabItem label="✅ Correct">

```ts
import type { User } from "./types";
export type { User };
```

</TabItem>
</Tabs>

### Inline Type Imports

<Tabs>
<TabItem label="❌ Incorrect">

```ts
import { type Post } from "./types";
export { Post };
```

</TabItem>
<TabItem label="✅ Correct">

```ts
import { type Post } from "./types";
export type { Post };
```

</TabItem>
</Tabs>

## Options

This rule is not configurable.

## When Not To Use It

If you prefer not to distinguish between type and value exports, you can disable this rule.

## Further Reading

- [TypeScript: Type-Only Imports and Exports](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-8.html#type-only-imports-and-export)

## Equivalents in Other Linters

<RuleEquivalents pluginId="ts" ruleId="typeExports" />
2 changes: 2 additions & 0 deletions packages/ts/src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,7 @@ import sequences from "./rules/sequences.ts";
import shadowedRestrictedNames from "./rules/shadowedRestrictedNames.ts";
import sparseArrays from "./rules/sparseArrays.ts";
import symbolDescriptions from "./rules/symbolDescriptions.ts";
import typeExports from "./rules/typeExports.ts";
import typeofComparisons from "./rules/typeofComparisons.ts";
import unassignedVariables from "./rules/unassignedVariables.ts";
import undefinedVariables from "./rules/undefinedVariables.ts";
Expand Down Expand Up @@ -465,6 +466,7 @@ export const ts = createPlugin({
shadowedRestrictedNames,
sparseArrays,
symbolDescriptions,
typeExports,
typeofComparisons,
unassignedVariables,
undefinedVariables,
Expand Down
56 changes: 56 additions & 0 deletions packages/ts/src/rules/typeExports.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import rule from "./typeExports.ts";
import { ruleTester } from "./ruleTester.ts";

Check failure on line 2 in packages/ts/src/rules/typeExports.test.ts

View workflow job for this annotation

GitHub Actions / Lint

Expected "./ruleTester.ts" to come before "./typeExports.ts"

ruleTester.describe(rule, {
invalid: [
{
code: `
import type { User } from "./types";
export { User };
`,
snapshot: `
import type { User } from "./types";
export { User };
~~~~~~~~~~~~~~~~
Use 'export type' for type-only exports.
`,
},
{
code: `
import type { User, Post } from "./types";
export { User, Post };
`,
snapshot: `
import type { User, Post } from "./types";
export { User, Post };
~~~~~~~~~~~~~~~~~~~~~~
Use 'export type' for type-only exports.
`,
},
{
code: `
import { type User } from "./types";
export { User };
`,
snapshot: `
import { type User } from "./types";
export { User };
~~~~~~~~~~~~~~~~
Use 'export type' for type-only exports.
`,
},
],
valid: [
`import type { User } from "./types";
export type { User };`,
`import { User } from "./types";
export { User };`,
`import { value } from "./module";
export { value };`,
`import type { User } from "./types";
import { createUser } from "./module";
export { createUser };`,
`export type { User } from "./types";`,
`export { value } from "./module";`,
],
});
80 changes: 80 additions & 0 deletions packages/ts/src/rules/typeExports.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import {
getTSNodeRange,
typescriptLanguage,
} from "@flint.fyi/typescript-language";
import ts from "typescript";

import { ruleCreator } from "./ruleCreator.ts";

export default ruleCreator.createRule(typescriptLanguage, {
about: {
description: "Reports exports that should use 'export type' syntax.",
id: "typeExports",
presets: ["stylistic"],
},
messages: {
useExportType: {
primary: "Use 'export type' for type-only exports.",
secondary: [
"This export only contains types, not runtime values.",
"Using 'export type' improves tree-shaking and makes intent clear.",
],
suggestions: ["Change to 'export type { ... }'."],
},
},
setup(context) {
const typeOnlyImports = new Set<string>();

return {
visitors: {
ImportDeclaration(node, { sourceFile }) {

Check failure on line 30 in packages/ts/src/rules/typeExports.ts

View workflow job for this annotation

GitHub Actions / Lint

'sourceFile' is defined but never used
if (node.importClause?.isTypeOnly) {
const namedBindings = node.importClause.namedBindings;
if (namedBindings && ts.isNamedImports(namedBindings)) {
for (const element of namedBindings.elements) {
typeOnlyImports.add(element.name.text);
}
}
if (node.importClause.name) {
typeOnlyImports.add(node.importClause.name.text);
}
} else if (
node.importClause?.namedBindings &&
ts.isNamedImports(node.importClause.namedBindings)
) {
for (const element of node.importClause.namedBindings.elements) {
if (element.isTypeOnly) {
typeOnlyImports.add(element.name.text);
}
}
}
},
ExportDeclaration(node, { sourceFile }) {

Check failure on line 52 in packages/ts/src/rules/typeExports.ts

View workflow job for this annotation

GitHub Actions / Lint

Expected "ExportDeclaration" to come before "ImportDeclaration"
if (node.isTypeOnly) {
return;
}

if (!node.exportClause || !ts.isNamedExports(node.exportClause)) {
return;
}

const allTypeOnly = node.exportClause.elements.every((element) => {
if (element.isTypeOnly) {
return true;
}
const exportedName =
element.propertyName?.text ?? element.name.text;
return typeOnlyImports.has(exportedName);
});

if (allTypeOnly && node.exportClause.elements.length > 0) {
context.report({
message: "useExportType",
range: getTSNodeRange(node, sourceFile),
});
}
},
},
};
},
});
Loading