Guide

ESLint fundamentals explained

ESLint is the de facto static analyzer for JavaScript and TypeScript. It walks your abstract syntax tree, applies hundreds of configurable rules, and flags logic bugs, unsafe patterns, and style drift before code merges. Unlike a formatter, ESLint reasons about program semantics: unreachable code, floating promises, incorrect hook usage, and import cycles are fair game. Modern projects use flat config (eslint.config.js) instead of the legacy .eslintrc cascade. This guide covers configuration, rule severity, plugins and shareable presets, typescript-eslint integration, IDE and CI workflows, a Harbor Commerce monorepo worked example, a tooling decision table, common pitfalls, and a production checklist. Pair it with TypeScript and your Vite build pipeline for end-to-end quality gates.

What ESLint is (and is not)

ESLint is a pluggable linter that parses source files into an AST, runs visitor functions registered by rules, and reports violations with file, line, column, and severity. It ships with core rules for ECMAScript best practices and lets the ecosystem add framework-specific checks through plugins.

It is not a code formatter. Prettier and Biome handle whitespace and quote style; ESLint focuses on correctness and maintainability. Many teams run both, using eslint-config-prettier to disable stylistic rules that would fight the formatter. ESLint also does not replace the TypeScript compiler: tsc --noEmit catches type errors; typescript-eslint adds type-aware lint rules on top.

Core concepts

  • Rule — a named check (e.g. no-unused-vars) with severity off, warn, or error and optional options.
  • Plugin — a package exporting additional rules and configs (e.g. @typescript-eslint, eslint-plugin-react-hooks).
  • Config object — in flat config, an exported array of config blocks, each with files, ignores, languageOptions, plugins, and rules.
  • Parser — converts source to AST; use @typescript-eslint/parser for .ts and .tsx files.
  • Processor — optional preprocessor for non-JS files (Vue SFCs, Markdown code blocks).

Flat config: the modern default

ESLint 9 made flat config the default. You export an array from eslint.config.js (or .mjs / .cjs). Each object in the array can target specific file globs, so test files, server code, and React components can carry different rule sets without fragile overrides nesting.

// eslint.config.js
import js from '@eslint/js';
import tseslint from 'typescript-eslint';
import reactHooks from 'eslint-plugin-react-hooks';

export default [
  { ignores: ['dist/**', 'node_modules/**', 'coverage/**'] },
  js.configs.recommended,
  ...tseslint.configs.recommended,
  {
    files: ['**/*.{ts,tsx}'],
    plugins: { 'react-hooks': reactHooks },
    rules: {
      'react-hooks/rules-of-hooks': 'error',
      'react-hooks/exhaustive-deps': 'warn',
      '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
    },
  },
];

Flat config resolves plugins and parsers as explicit imports — no more mystery extends chains. Shareable configs (like typescript-eslint presets) spread into the array with .... Use ignores at the top level for build artifacts; per-block files scopes rules to the right directories.

Migrating from .eslintrc

Run npx @eslint/migrate-config .eslintrc.cjs for a starting point, then hand-tune. Legacy env keys become languageOptions.globals from the globals package. Root true is implicit in flat config. Delete .eslintignore in favor of ignores arrays.

Rules, severity, and autofix

Each rule ID maps to a severity and optional configuration array. Start from a recommended preset, then tighten or relax per team norms.

  • error — fails eslint CLI and CI; blocks merge.
  • warn — surfaces in IDE squiggles without failing CI unless you use --max-warnings 0.
  • off — disables the rule entirely.

Many rules support autofix via eslint --fix. Safe fixes (import ordering, const preference) can run in pre-commit hooks. Dangerous fixes stay manual. Use --fix-dry-run in CI logs to preview bulk changes before applying them on a branch.

High-value core rules

  • no-undef / no-unused-vars — catch dead code and typos (TypeScript projects often delegate to @typescript-eslint variants).
  • eqeqeq — require === instead of loose equality.
  • no-floating-promises (typescript-eslint) — unhandled async calls are a top production bug source.
  • import/no-cycle (eslint-plugin-import) — breaks circular dependency graphs early.
  • no-console — warn in app code; allow in scripts and tests via overrides.

typescript-eslint: typed linting

The typescript-eslint project provides a parser, plugin, and typed rule sets. configs.recommended covers syntax-safe checks without type information. configs.recommendedTypeChecked enables rules that read the TypeScript program — slower but catches real bugs like unsafe any flows and mis-typed promises.

// Type-aware block — point parserOptions.project at tsconfig
{
  files: ['src/**/*.ts'],
  languageOptions: {
    parserOptions: {
      projectService: true, // ESLint 9+ — auto-discovers tsconfig
      tsconfigRootDir: import.meta.dirname,
    },
  },
  rules: {
    '@typescript-eslint/no-misused-promises': 'error',
    '@typescript-eslint/await-thenable': 'error',
  },
}

Type-aware linting needs the TypeScript compiler on disk. In monorepos, scope projectService or explicit project arrays per package to avoid parsing the entire repo on every file save. Run type-aware rules in CI; optional locally on save for speed.

Plugins and framework presets

Framework ecosystems ship ESLint plugins that encode community best practices as machine-checkable rules:

  • eslint-plugin-react — JSX conventions, prop-types legacy, display names.
  • eslint-plugin-react-hooks — Rules of Hooks and exhaustive effect dependencies.
  • eslint-plugin-vue — SFC structure, template syntax, Vue 3 composition API patterns.
  • @angular-eslint — component selectors, lifecycle hooks, template accessibility.
  • eslint-plugin-import — unresolved imports, duplicate paths, ordered groups.
  • eslint-plugin-jsx-a11y — alt text, ARIA roles, keyboard-focusable elements.

Prefer official or widely adopted presets over copying random rule lists from blog posts. Extend with team-specific rules only when a repeated code-review comment can be automated.

IDE integration and developer workflow

The ESLint VS Code extension reads your flat config and shows violations inline. Enable format on save only for the formatter; let ESLint autofix run as a separate code-action or lint-on-save step to avoid fighting Prettier.

  • Commit eslint.config.js to the repo — single source of truth for local and CI.
  • Add "eslint.validate" for Vue, MDX, or JSON if plugins support them.
  • Use eslint-disable-next-line sparingly with a comment explaining why; never blanket-disable files.
  • Wire npm run lint as eslint . and expose lint:fix for local cleanup.

In monorepos, run ESLint from the root with a config that uses files globs per package, or invoke per-package scripts via Turborepo / Nx with cached task outputs.

CI gates and pre-commit hooks

Treat lint like a test suite: it must pass before deploy. A minimal CI step:

npm ci
npm run lint   # eslint . --max-warnings 0
npm test

lint-staged with Husky runs ESLint only on changed files for fast commits:

// package.json
"lint-staged": {
  "*.{js,ts,tsx}": "eslint --fix --max-warnings 0"
}

Cache node_modules and ESLint cache (eslint --cache) in GitHub Actions to keep PR feedback under a minute. Fail the build on new warnings if your team treats warnings as tech debt.

Worked example: Harbor Commerce monorepo

Harbor Commerce ships a packages/api Node service, a packages/web React storefront, and shared packages/types. One root eslint.config.js applies baseline TypeScript rules everywhere, then layers framework plugins per package.

  1. Shared baseline@eslint/js recommended plus typescript-eslint strict; ban console.log in packages/ except scripts/.
  2. API package — enable no-floating-promises and @typescript-eslint/require-await; Node globals from globals/node.
  3. Web package — add react-hooks and jsx-a11y; browser globals; relax no-console in *.test.tsx.
  4. Types package — type-only exports; rule @typescript-eslint/consistent-type-exports keeps public API surfaces clean.
  5. CI — Turbo pipeline runs lint in parallel per package; root job fails if any package reports errors.

When the team adopted flat config, they deleted six nested .eslintrc files and cut lint CI time 30% by enabling --cache and scoping type-aware rules to src/ only. New engineers get identical squiggles locally and in the PR check because the config lives in git, not editor preferences.

Tooling decision table

NeedReach forWhy
JS/TS correctness and framework rulesESLint + pluginsLargest rule ecosystem; flat config; framework-first plugins
Formatter-only consistencyPrettier or Biome formatOpinionated layout; no semantic analysis
All-in-one lint + format + fast rewriteBiomeRust speed; smaller plugin surface; good for greenfield
Type errors onlytsc --noEmitCompiler is authoritative; lint supplements, not replaces
Legacy AngularJS / old ESLint 8Migrate to flat configESLint 9+ drops eslintrc; security and support

Common pitfalls

  • Disabling rules instead of fixingeslint-disable comments accumulate; schedule rule-enabling sprints.
  • Type-aware lint on huge monorepos without scoping — parse only relevant tsconfig paths or CI becomes painfully slow.
  • Duplicating Prettier in ESLint rules — use eslint-config-prettier to avoid format tug-of-war.
  • Stale shareable configseslint-config-airbnb and friends may lag ESLint 9; verify flat-config support.
  • Linting build output — ignore dist/, .next/, and generated SDK folders.
  • Warnings ignored forever — either fix them or promote to errors; warnings in CI become invisible noise.
  • Missing CI parity — if only one developer runs ESLint locally, bugs slip through; enforce on every PR.

Production checklist

  • Adopt eslint.config.js flat config on ESLint 9+.
  • Ignore build artifacts, coverage, and vendored code in top-level ignores.
  • Use typescript-eslint for all .ts / .tsx files.
  • Enable framework plugins (React Hooks, Vue, etc.) where applicable.
  • Separate formatter (Prettier/Biome) from linter; disable conflicting stylistic rules.
  • Run eslint . --max-warnings 0 in CI with caching.
  • Add lint-staged + Husky for fast pre-commit feedback on changed files.
  • Document how to add a justified eslint-disable-next-line in CONTRIBUTING.md.
  • Scope type-aware rules to production source, not every config file.
  • Re-run eslint --fix after major dependency upgrades.

Key takeaways

  • ESLint catches semantic bugs and enforces team conventions; it complements TypeScript and formatters.
  • Flat config replaces .eslintrc with explicit, composable config arrays per file glob.
  • typescript-eslint adds type-aware rules when wired to your tsconfig.
  • Plugins encode React, Vue, import, and accessibility best practices as automated checks.
  • Run ESLint in CI with zero warnings tolerance; cache results for speed in monorepos.

Related reading