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 severityoff,warn, orerrorand 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, andrules. - Parser — converts source to AST; use
@typescript-eslint/parserfor.tsand.tsxfiles. - 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
eslintCLI 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-eslintvariants).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.jsto 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-linesparingly with a comment explaining why; never blanket-disable files. - Wire
npm run lintaseslint .and exposelint:fixfor 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.
- Shared baseline —
@eslint/jsrecommended plustypescript-eslintstrict; banconsole.loginpackages/exceptscripts/. - API package — enable
no-floating-promisesand@typescript-eslint/require-await; Node globals fromglobals/node. - Web package — add
react-hooksandjsx-a11y; browser globals; relaxno-consolein*.test.tsx. - Types package — type-only exports; rule
@typescript-eslint/consistent-type-exportskeeps public API surfaces clean. - CI — Turbo pipeline runs
lintin 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
| Need | Reach for | Why |
|---|---|---|
| JS/TS correctness and framework rules | ESLint + plugins | Largest rule ecosystem; flat config; framework-first plugins |
| Formatter-only consistency | Prettier or Biome format | Opinionated layout; no semantic analysis |
| All-in-one lint + format + fast rewrite | Biome | Rust speed; smaller plugin surface; good for greenfield |
| Type errors only | tsc --noEmit | Compiler is authoritative; lint supplements, not replaces |
| Legacy AngularJS / old ESLint 8 | Migrate to flat config | ESLint 9+ drops eslintrc; security and support |
Common pitfalls
- Disabling rules instead of fixing —
eslint-disablecomments 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-prettierto avoid format tug-of-war. - Stale shareable configs —
eslint-config-airbnband 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.jsflat config on ESLint 9+. - Ignore build artifacts, coverage, and vendored code in top-level
ignores. - Use
typescript-eslintfor all.ts/.tsxfiles. - Enable framework plugins (React Hooks, Vue, etc.) where applicable.
- Separate formatter (Prettier/Biome) from linter; disable conflicting stylistic rules.
- Run
eslint . --max-warnings 0in CI with caching. - Add lint-staged + Husky for fast pre-commit feedback on changed files.
- Document how to add a justified
eslint-disable-next-linein CONTRIBUTING.md. - Scope type-aware rules to production source, not every config file.
- Re-run
eslint --fixafter major dependency upgrades.
Key takeaways
- ESLint catches semantic bugs and enforces team conventions; it complements TypeScript and formatters.
- Flat config replaces
.eslintrcwith 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
- TypeScript fundamentals explained — types the linter and compiler both rely on
- Vite fundamentals explained — wire
npm run lintbeside dev and build scripts - Vitest fundamentals explained — lint test files with relaxed rules via
filesoverrides - Node.js fundamentals explained — server-side globals and async patterns ESLint should enforce