@tnnquang/eslint
v3.0.1
Published
Opinionated ESLint plugin for modern TypeScript projects. Enforces explicit imports, function declarations, path aliases, and consistent type imports across React, Vue, Angular, NestJS, Next.js, and Nuxt.
Maintainers
Readme
@tnnquang/eslint
Opinionated ESLint plugin for modern TypeScript projects. Enforces explicit imports, function declarations, path aliases, and consistent type imports.
Supports ESLint v7–v9+ (both legacy eslintrc and flat config formats).
Philosophy
This plugin enforces patterns that improve code quality at scale:
- Explicit over implicit — Named imports make dependencies visible; function declarations are hoisted and produce better stack traces.
- Consistency over flexibility — Path aliases eliminate fragile relative paths; type imports clarify the value/type boundary.
- Performance-aware — Named imports enable tree-shaking; avoiding barrel files prevents bundler slowdowns and circular deps.
- Framework-aware — Rules adapt to your framework's conventions automatically.
Rules
| Rule | Fixable | Description |
|------|---------|-------------|
| no-arrow-components | ✅ | Enforce function declarations for top-level functions and components |
| no-namespace-import | ✅ | Disallow import * as X and default-as-namespace usage |
| enforce-path-alias | ✅ | Enforce path aliases instead of deep relative imports |
| no-barrel-imports | — | Disallow importing from barrel/index files within the same module |
| consistent-type-imports | ✅ | Enforce consistent import type usage in TypeScript |
Installation
npm install --save-dev @tnnquang/eslint
# or
yarn add -D @tnnquang/eslint
# or
pnpm add -D @tnnquang/eslintUsage
Flat Config (ESLint v9+ — recommended)
// eslint.config.js
import tnnquang from "@tnnquang/eslint";
export default [
tnnquang.configs["flat/recommended"],
// or framework-specific:
// tnnquang.configs["flat/react"],
// tnnquang.configs["flat/vue"],
// tnnquang.configs["flat/typescript"],
];Available flat configs: flat/recommended, flat/strict, flat/react, flat/vue, flat/angular, flat/nestjs, flat/nextjs, flat/nuxt, flat/typescript.
Legacy Config (eslintrc)
{
"extends": ["plugin:@tnnquang/eslint/recommended"]
}Available legacy configs: recommended, strict, react, vue, angular, nestjs, nextjs, nuxt, typescript.
Manual Configuration
// eslint.config.js (flat)
import tnnquang from "@tnnquang/eslint";
export default [
{
plugins: { "@tnnquang/eslint": tnnquang },
rules: {
"@tnnquang/eslint/no-arrow-components": "warn",
"@tnnquang/eslint/no-namespace-import": ["error", {
allowedStarImports: ["@sentry/*"],
allowedLibraries: ["lodash-es"],
}],
"@tnnquang/eslint/enforce-path-alias": ["warn", {
minRelativeDepth: 2,
}],
"@tnnquang/eslint/no-barrel-imports": "warn",
"@tnnquang/eslint/consistent-type-imports": ["warn", {
prefer: "type-imports",
}],
},
},
];Rule Details
no-arrow-components
Enforces function declarations for top-level functions and React components. Provides autofix.
Options
{
// Allow arrow functions for non-component top-level functions
allowArrowFunctions?: boolean; // default: false
// Allow arrow functions as arguments to HOCs (memo, forwardRef, styled, etc.)
allowInCallExpressions?: boolean; // default: true
// Custom list of HOC/wrapper names that can wrap arrow functions
allowedWrappers?: string[]; // default: ["memo", "forwardRef", "React.memo", ...]
// Allow arrow functions with explicit type annotation (TypeScript)
allowTypedFunctionExpressions?: boolean; // default: true
// Allow arrow functions in `export default () => {}`
allowExportDefault?: boolean; // default: false
// Exclude functions matching these patterns (regex)
exclude?: string[]; // e.g., ["^use[A-Z]", ".*Handler$"]
// Only check functions matching these patterns (regex)
include?: string[];
// Enable/disable for TypeScript files
checkTypeScript?: boolean; // default: true
}Examples
// ❌ Incorrect
const MyComponent = () => <div>Hello</div>;
const fetchData = async (url) => { return fetch(url); };
export default () => <App />;
// ✅ Correct
function MyComponent() { return <div>Hello</div>; }
async function fetchData(url) { return fetch(url); }
export default function() { return <App />; }
// ✅ Allowed (HOC wrappers)
const MemoComponent = memo(() => <div/>);
const StyledButton = styled.button`color: red`;
const Connected = connect(mapState)(MyComponent);
// ✅ Allowed (typed function expression in TypeScript)
const handler: RequestHandler = (req, res) => { res.send("ok"); };no-namespace-import
Disallows both import * as X and default imports used as namespaces (import X; X.method()). Provides autofix.
Options
{
// Libraries fully excluded from all checks (supports glob patterns)
allowedLibraries?: string[]; // e.g., ["@sentry/*", "lodash-es"]
// Only check these libraries (empty = check all)
targetLibraries?: string[]; // e.g., ["react", "lodash", "redux"]
// Allow `import * as X` globally
allowStarImports?: boolean; // default: false
// Allow default-as-namespace globally
allowDefaultAsNamespace?: boolean; // default: false
// Libraries allowed to use `import * as` specifically
allowedStarImports?: string[]; // e.g., ["@sentry/*", "protobufjs"]
// Libraries allowed to use default as namespace
allowedDefaultAsNamespace?: string[]; // e.g., ["moment", "dayjs"]
// Check TypeScript type imports
checkTypeScriptTypes?: boolean; // default: true
// Allow namespace for type-only imports
allowTypeNamespaces?: boolean; // default: false
// Ignore all external (non-relative) imports
ignoreExternalModules?: boolean; // default: false
}Examples
// ❌ Incorrect
import * as Sentry from "@sentry/browser"; // unless in allowedStarImports
import React from "react";
React.useState(0); // default used as namespace
// ✅ Correct
import { init, captureException } from "@sentry/browser";
import { useState } from "react";
// ✅ With config: allowedStarImports: ["@sentry/*"]
import * as Sentry from "@sentry/browser"; // allowed by configGlob Pattern Support
All library lists support glob-style wildcards:
"@sentry/*"matches@sentry/browser,@sentry/node, etc."lodash*"matcheslodash,lodash-es,lodash/fp"@company/*"matches all packages in the scope
enforce-path-alias
Enforces path aliases for imports instead of deep relative paths. Auto-detects from tsconfig.json, vite.config.*. Provides autofix.
Options
{
// 'all': any relative import within baseUrl
// 'direct-children': only imports to other top-level folders
mode?: "all" | "direct-children"; // default: "direct-children"
// Minimum ../ depth to trigger (1 = any parent traversal)
minRelativeDepth?: number; // default: 1
// Config file to read aliases from
configFile?: string; // default: "tsconfig.json"
// Manual path aliases (overrides auto-detection)
paths?: Record<string, string[]>;
// Base URL for resolving
baseUrl?: string; // auto-detected if omitted
// Folder patterns to exclude
exclude?: string[]; // e.g., ["test", "spec", "__mocks__"]
// File patterns to skip entirely
excludeFiles?: string[]; // e.g., ["*.config.*", "*.spec.*"]
// Allow ./ imports (sibling imports)
allowRelativeInSameFolder?: boolean; // default: true
// Supported file extensions
supportedExtensions?: string[];
// Include .d.ts files
includeDeclarationFiles?: boolean; // default: false
// Check dynamic import() expressions
includeDynamicImports?: boolean; // default: true
}Examples
// tsconfig.json: { "paths": { "@/*": ["./src/*"] } }
// ❌ Incorrect (in src/components/Button.tsx)
import { validate } from "../../utils/validation";
import { API_URL } from "../../config/constants";
const module = await import("../../services/api");
// ✅ Correct
import { validate } from "@/utils/validation";
import { API_URL } from "@/config/constants";
const module = await import("@/services/api");
// ✅ Allowed (same folder, allowRelativeInSameFolder: true)
import { ButtonProps } from "./Button.types";no-barrel-imports
Disallows importing from barrel/index files within the same module to prevent circular dependencies. Not auto-fixable (ambiguous target).
Options
{
// Scope of detection
scope?: "same-module" | "same-parent" | "all-relative"; // default: "same-parent"
// File names considered barrels
barrelFiles?: string[]; // default: ["index"]
// Import patterns allowed even if they hit a barrel
allowedPatterns?: string[]; // e.g., ["@/store", "@/api"]
// Folders to exclude
excludeFolders?: string[]; // default: ["node_modules"]
// Also check aliased imports
checkAliasImports?: boolean; // default: false
}Examples
// src/components/Button/Button.tsx
// ❌ Incorrect (resolves to Icon/index.ts barrel)
import { Icon } from "../Icon";
// ✅ Correct (direct import)
import { Icon } from "../Icon/Icon";consistent-type-imports
Enforces consistent import type usage in TypeScript. Only active for .ts/.tsx files. Provides autofix.
Options
{
// Which style to enforce
prefer?: "type-imports" | "inline-type-imports" | "no-type-imports"; // default: "type-imports"
// Disallow type annotations in value imports
disallowTypeAnnotations?: boolean; // default: true
// Fix style
fixStyle?: "separate-type-imports" | "inline-type-imports";
// Libraries to exclude (e.g., with side effects)
allowedLibraries?: string[]; // supports glob
}Examples
// prefer: "type-imports" (default)
// ❌ Incorrect
import { type User, type Post } from "./types";
// ✅ Correct
import type { User, Post } from "./types";
// prefer: "inline-type-imports"
// ❌ Incorrect
import type { User } from "./types";
// ✅ Correct
import { type User } from "./types";Configuration Recipes
React + TypeScript (strict)
// eslint.config.js
import tnnquang from "@tnnquang/eslint";
export default [
tnnquang.configs["flat/strict"],
{
rules: {
"@tnnquang/eslint/no-namespace-import": ["error", {
allowedStarImports: ["@testing-library/*"],
allowedDefaultAsNamespace: ["classnames"],
}],
"@tnnquang/eslint/no-arrow-components": ["error", {
exclude: ["^use[A-Z]"], // allow hooks as arrow functions
}],
},
},
];Vue/Nuxt
import tnnquang from "@tnnquang/eslint";
export default [
tnnquang.configs["flat/vue"],
{
rules: {
"@tnnquang/eslint/enforce-path-alias": ["warn", {
configFile: "tsconfig.json",
exclude: ["composables", "stores"],
}],
},
},
];NestJS Backend
import tnnquang from "@tnnquang/eslint";
export default [
tnnquang.configs["flat/nestjs"],
{
rules: {
"@tnnquang/eslint/no-namespace-import": ["error", {
allowedStarImports: ["@nestjs/common", "@nestjs/core"],
}],
"@tnnquang/eslint/consistent-type-imports": ["warn", {
prefer: "inline-type-imports",
}],
},
},
];Monorepo with custom aliases
import tnnquang from "@tnnquang/eslint";
export default [
tnnquang.configs["flat/typescript"],
{
rules: {
"@tnnquang/eslint/enforce-path-alias": ["error", {
mode: "all",
minRelativeDepth: 2,
paths: {
"@app/*": ["./src/*"],
"@shared/*": ["./packages/shared/src/*"],
"@ui/*": ["./packages/ui/src/*"],
},
exclude: ["__tests__", "__mocks__"],
}],
},
},
];Migration from v2
Breaking Changes in v3
- New rules added:
no-barrel-importsandconsistent-type-imports no-namespace-importnow catchesimport * as— add exceptions withallowedStarImportsno-arrow-componentsnow has autofix — runeslint --fixto auto-convert- Node.js 18+ required
- Flat config support — use
flat/*config names for ESLint v9+
Migration Steps
// If you used v2 and have libraries that use import * as:
"@tnnquang/eslint/no-namespace-import": ["error", {
+ allowedStarImports: ["@sentry/*", "protobufjs"],
+ allowedDefaultAsNamespace: ["moment", "dayjs"],
}]License
MIT
Author
Tran Ngoc Nhat Quang
Contributing
Issues and pull requests are welcome at github.com/tnnquang/eslint-plugin.
