diff --git a/.prettierrc b/.prettierrc
new file mode 100644
index 0000000..4632906
--- /dev/null
+++ b/.prettierrc
@@ -0,0 +1,6 @@
+{
+ "tabWidth": 4,
+ "useTabs": false,
+ "trailingComma": "es5",
+ "arrowParens": "always"
+}
diff --git a/README.md b/README.md
index 0bf2109..ecae151 100644
--- a/README.md
+++ b/README.md
@@ -9,6 +9,7 @@ Custom ESLint rules used internally at Meitner
- [no-use-prefix-for-non-hook](#no-use-prefix-for-non-hook)
- [no-react-namespace](#no-react-namespace)
- [no-literal-jsx-style-prop-values](#no-literal-jsx-style-prop-values)
+- [no-exported-types-outside-types-file](#no-exported-types-outside-types-file)
### no-inline-function-parameter-type-annotation
@@ -178,3 +179,35 @@ Examples of invalid code
```ts
```
+
+### no-exported-types-in-tsx-files
+
+Exporting your types from your component's tsx file can lead to dependency loops and make your code harder to maintain.
+
+This rule forbids exporting types from tsx files.
+
+Examples of valid code
+
+```ts
+// MyComponent.tsx
+import { Props } from "./MyComponent.types";
+
+export default function MyComponent(props: Props) {
+ return
{props.children}
;
+}
+```
+
+Examples of invalid code
+
+```ts
+// MyComponent.tsx
+import { Props } from "./MyComponent.types";
+
+export default function MyComponent(props: Props) { // error
+ return {props.children}
;
+}
+
+export type Props = {
+ children: ReactNode;
+};
+```
diff --git a/package.json b/package.json
index 2a070c4..d27285b 100644
--- a/package.json
+++ b/package.json
@@ -19,6 +19,7 @@
},
"devDependencies": {
"@types/eslint": "^8.56.7",
+ "@types/node": "^22.7.9",
"@typescript-eslint/eslint-plugin": "^7.8.0",
"@typescript-eslint/parser": "^7.8.0",
"@typescript-eslint/rule-tester": "7.5.0",
@@ -30,5 +31,8 @@
},
"peerDependencies": {
"eslint": "8.56.0"
+ },
+ "dependencies": {
+ "prettier": "^3.3.3"
}
}
diff --git a/src/rules/index.ts b/src/rules/index.ts
index 23d4a0d..e382c34 100644
--- a/src/rules/index.ts
+++ b/src/rules/index.ts
@@ -1,4 +1,5 @@
import { alwaysSpreadJSXPropsFirst } from "./alwaysSpreadJSXPropsFirst";
+import { noExportedTypesInTsxFiles } from "./noExportedTypesInTsxFiles";
import { noInlineFunctionParameterTypeAnnotation } from "./noInlineFunctionParameterTypeAnnotation";
import { noLiteralJSXStylePropValues } from "./noLiteralJSXStylePropValues";
import { noMixedExports } from "./noMixedExports";
@@ -13,6 +14,7 @@ const rules = {
"no-react-namespace": noReactNamespace,
"no-literal-jsx-style-prop-values": noLiteralJSXStylePropValues,
"always-spread-props-first": alwaysSpreadJSXPropsFirst,
+ "no-exported-types-in-tsx-files": noExportedTypesInTsxFiles,
};
export { rules };
diff --git a/src/rules/noExportedTypesInTsxFiles.ts b/src/rules/noExportedTypesInTsxFiles.ts
new file mode 100644
index 0000000..e26d527
--- /dev/null
+++ b/src/rules/noExportedTypesInTsxFiles.ts
@@ -0,0 +1,40 @@
+import { ESLintUtils, TSESTree } from "@typescript-eslint/utils";
+
+export const noExportedTypesInTsxFiles = ESLintUtils.RuleCreator.withoutDocs({
+ create(context) {
+ const filename = context.filename;
+ const isTsxFile = filename.endsWith(".tsx");
+
+ return {
+ TSInterfaceDeclaration(node: TSESTree.TSInterfaceDeclaration) {
+ checkExportedType(node);
+ },
+ TSTypeAliasDeclaration(node: TSESTree.TSTypeAliasDeclaration) {
+ checkExportedType(node);
+ },
+ TSEnumDeclaration(node: TSESTree.TSEnumDeclaration) {
+ checkExportedType(node);
+ },
+ };
+
+ function checkExportedType(node: TSESTree.Node) {
+ const isExported = node.parent?.type === "ExportNamedDeclaration";
+
+ if (isExported && isTsxFile) {
+ context.report({
+ node,
+ messageId: "noExportedTypesInTsxFiles",
+ });
+ }
+ }
+ },
+ meta: {
+ type: "problem",
+ messages: {
+ noExportedTypesInTsxFiles:
+ "Exported types are not allowed in '.tsx' files.",
+ },
+ schema: [],
+ },
+ defaultOptions: [],
+});
diff --git a/src/tests/noExportedTypesInTsxFiles.ts b/src/tests/noExportedTypesInTsxFiles.ts
new file mode 100644
index 0000000..d46da7d
--- /dev/null
+++ b/src/tests/noExportedTypesInTsxFiles.ts
@@ -0,0 +1,70 @@
+import { RuleTester } from "@typescript-eslint/rule-tester";
+import * as vitest from "vitest";
+import { noExportedTypesInTsxFiles } from "../rules/noExportedTypesInTsxFiles";
+
+RuleTester.afterAll = vitest.afterAll;
+RuleTester.it = vitest.it;
+RuleTester.itOnly = vitest.it.only;
+RuleTester.describe = vitest.describe;
+
+const ruleTester = new RuleTester({
+ parser: "@typescript-eslint/parser",
+});
+
+ruleTester.run("noExportedTypesInTsxFiles", noExportedTypesInTsxFiles, {
+ valid: [
+ {
+ code: `
+ export interface MyInterface {
+ prop: string;
+ }
+ `,
+ filename: "types.ts",
+ parserOptions: { ecmaVersion: 2020, sourceType: "module" },
+ },
+ {
+ code: `
+ interface MyInterface {
+ prop: string;
+ }
+ `,
+ filename: "component.tsx",
+ parserOptions: { ecmaVersion: 2020, sourceType: "module" },
+ },
+ {
+ code: `
+ export type MyType = string;
+ `,
+ filename: "someFile.ts",
+ parserOptions: { ecmaVersion: 2020, sourceType: "module" },
+ },
+ ],
+ invalid: [
+ {
+ code: `
+ export interface MyInterface {
+ prop: string;
+ }
+ `,
+ filename: "component.tsx",
+ parserOptions: { ecmaVersion: 2020, sourceType: "module" },
+ errors: [
+ {
+ messageId: "noExportedTypesInTsxFiles",
+ },
+ ],
+ },
+ {
+ code: `
+ export type MyType = string;
+ `,
+ filename: "component.tsx",
+ parserOptions: { ecmaVersion: 2020, sourceType: "module" },
+ errors: [
+ {
+ messageId: "noExportedTypesInTsxFiles",
+ },
+ ],
+ },
+ ],
+});
diff --git a/yarn.lock b/yarn.lock
index 713923f..e04d2df 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -304,6 +304,13 @@
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841"
integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==
+"@types/node@^22.7.9":
+ version "22.7.9"
+ resolved "https://registry.yarnpkg.com/@types/node/-/node-22.7.9.tgz#2bf2797b5e84702d8262ea2cf843c3c3c880d0e9"
+ integrity sha512-jrTfRC7FM6nChvU7X2KqcrgquofrWLFDeYC1hKfwNWomVvrn7JIksqf344WN2X/y8xrgqBd2dJATZV4GbatBfg==
+ dependencies:
+ undici-types "~6.19.2"
+
"@types/semver@^7.5.0", "@types/semver@^7.5.8":
version "7.5.8"
resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.8.tgz#8268a8c57a3e4abd25c165ecd36237db7948a55e"
@@ -1333,6 +1340,11 @@ prelude-ls@^1.2.1:
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==
+prettier@^3.3.3:
+ version "3.3.3"
+ resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.3.3.tgz#30c54fe0be0d8d12e6ae61dbb10109ea00d53105"
+ integrity sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==
+
pretty-format@^29.7.0:
version "29.7.0"
resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.7.0.tgz#ca42c758310f365bfa71a0bda0a807160b776812"
@@ -1544,6 +1556,11 @@ ufo@^1.3.2:
resolved "https://registry.yarnpkg.com/ufo/-/ufo-1.5.3.tgz#3325bd3c977b6c6cd3160bf4ff52989adc9d3344"
integrity sha512-Y7HYmWaFwPUmkoQCUIAYpKqkOf+SbVj/2fJJZ4RJMCfZp0rTGwRbzQD+HghfnhKOjL9E01okqz+ncJskGYfBNw==
+undici-types@~6.19.2:
+ version "6.19.8"
+ resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.19.8.tgz#35111c9d1437ab83a7cdc0abae2f26d88eda0a02"
+ integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==
+
uri-js@^4.2.2:
version "4.4.1"
resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e"