← Back to articles
Tooling ESLintTypeScriptDX

ESLint: Enforcing Code Quality Across TypeScript Projects

· 3 min read

Code reviews shouldn’t waste time on formatting arguments or import order debates. ESLint automates the enforcement of code quality rules across a TypeScript project so developers can focus on logic, architecture, and actual bugs. With the flat config format introduced in v9, setting up ESLint is cleaner than ever, and the TypeScript integration has matured to the point where type-aware rules catch issues that the compiler alone misses.

Flat config setup

The flat config replaces the old .eslintrc chain with a single eslint.config.ts file. No more config cascading, no more extends arrays, no more confusion about which rules apply where.

import eslint from '@eslint/js';
import tseslint from 'typescript-eslint';
import reactPlugin from 'eslint-plugin-react';
import reactHooksPlugin from 'eslint-plugin-react-hooks';

export default tseslint.config(
  eslint.configs.recommended,
  ...tseslint.configs.recommendedTypeChecked,
  {
    languageOptions: {
      parserOptions: {
        projectService: true,
        tsconfigRootDir: import.meta.dirname,
      },
    },
  },
  {
    plugins: { react: reactPlugin, 'react-hooks': reactHooksPlugin },
    rules: {
      'react/jsx-no-target-blank': 'error',
      'react-hooks/rules-of-hooks': 'error',
      'react-hooks/exhaustive-deps': 'warn',
    },
  },
  { ignores: ['.next/', 'node_modules/', 'dist/'] }
);

recommendedTypeChecked enables rules that use the TypeScript compiler’s type information. These catch real bugs like unsafe any usage, floating promises, and incorrect nullability assumptions that basic linting misses entirely.

Type-aware rules that catch real bugs

Standard ESLint rules operate on syntax. Type-aware rules operate on semantics. The difference is catching a missing await on a function that returns a Promise versus ignoring it because the syntax is valid JavaScript.

// @typescript-eslint/no-floating-promises catches this
async function deleteProject(id: string) {
  // Bug: promise is not awaited, errors are silently swallowed
  db.project.delete({ where: { id } });
}

// @typescript-eslint/no-unsafe-assignment catches this
const config: Config = JSON.parse(rawInput); // any assigned to typed variable

no-floating-promises alone justifies enabling type-aware linting. Unhandled promises are one of the most common sources of silent failures in Node.js and Next.js applications. The TypeScript compiler doesn’t flag them, but ESLint does.

Custom rules for project conventions

Every codebase has conventions that no public plugin covers. Maybe barrel imports are banned for performance reasons, or certain directories should only import from specific modules. ESLint’s rule API lets you encode these as automated checks.

{
  rules: {
    'no-restricted-imports': [
      'error',
      {
        patterns: [
          {
            group: ['@/features/*/internal/*'],
            message: 'Import from the feature public API, not internal modules.',
          },
        ],
      },
    ],
    'no-restricted-syntax': [
      'error',
      {
        selector: 'CallExpression[callee.name="useEffect"][arguments.length=1]',
        message: 'useEffect must have a dependency array.',
      },
    ],
  },
}

no-restricted-imports enforces module boundaries. no-restricted-syntax uses AST selectors to ban specific code patterns. Together they turn architectural decisions into automated guardrails that catch violations at lint time instead of during code review.

CI integration

Running ESLint locally is optional. Running it in CI is not. A lint step in the pipeline ensures that no code merges without passing the rules, regardless of individual editor setups or forgotten pre-commit hooks.

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
      - run: npm ci
      - run: npx eslint . --max-warnings 0

--max-warnings 0 treats warnings as errors in CI. This prevents the gradual accumulation of warnings that everyone ignores until there are hundreds. Rules are either enforced or they’re not. The gray zone of warnings only works during migration periods.

Conclusion

ESLint transforms code conventions from opinions into automated checks. Type-aware rules catch bugs the compiler misses, custom rules enforce architectural boundaries, and CI integration makes compliance non-negotiable. For TypeScript and React projects, a well-configured ESLint setup is the single most effective tool for maintaining code quality across a team.