ESLint is the most widely used JavaScript linter in the world, and in 2026 it has settled into a mature, stable toolchain with flat config as the standard, excellent TypeScript support, and tight editor integration. The old .eslintrc format is deprecated — if you have not migrated to eslint.config.js, now is the time. This guide covers a practical setup from scratch to CI.
What changed in 2026
- ESLint 9 made flat config the default and deprecated
.eslintrc. Migration is straightforward for most projects; the @eslint/migrate-config tool handles 80% of it automatically.
@typescript-eslint v8 unified the two packages (@typescript-eslint/parser and @typescript-eslint/eslint-plugin) under a single typescript-eslint import.
- ESLint runs in parallel with the new worker-based architecture — lint times on large codebases dropped by 30–50%.
- Biome emerged as a faster alternative for teams that want a single tool for linting and formatting, though ESLint still has broader rule coverage.
Install
npm install --save-dev eslint typescript-eslint
# For Prettier integration:
npm install --save-dev prettier eslint-config-prettier
Basic flat config (JavaScript)
// eslint.config.js
import js from '@eslint/js';
export default [
js.configs.recommended,
{
rules: {
'no-console': 'warn',
'no-unused-vars': 'error',
'prefer-const': 'error',
},
},
];
TypeScript setup (recommended)
// eslint.config.js
import js from '@eslint/js';
import tseslint from 'typescript-eslint';
import prettier from 'eslint-config-prettier';
export default tseslint.config(
js.configs.recommended,
...tseslint.configs.recommendedTypeChecked,
{
languageOptions: {
parserOptions: {
project: true, // use tsconfig.json
tsconfigRootDir: import.meta.dirname,
},
},
},
prettier, // disable ESLint rules that conflict with Prettier
{
rules: {
'@typescript-eslint/no-floating-promises': 'error',
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/consistent-type-imports': 'error',
'no-console': ['warn', { allow: ['warn', 'error'] }],
},
},
{
ignores: ['dist/', 'node_modules/', '*.d.ts'],
},
);
The rules that matter most for TypeScript
| Rule |
Why it matters |
@typescript-eslint/no-floating-promises |
Unhandled promises are a common source of silent failures |
@typescript-eslint/no-misused-promises |
Prevents passing async functions where sync ones are expected |
@typescript-eslint/consistent-type-imports |
Enforces import type for type-only imports — better tree-shaking |
@typescript-eslint/no-explicit-any |
Keeps the type system honest |
no-unused-vars (TS version) |
Dead code left in after refactoring |
prefer-const |
Forces immutable-by-default declarations |
Prettier integration (do it right)
Run ESLint and Prettier as separate tools — do not use eslint-plugin-prettier:
// package.json scripts
{
"scripts": {
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"format": "prettier --write .",
"format:check": "prettier --check ."
}
}
eslint-config-prettier (the last entry in your config) disables the ESLint rules that would conflict with Prettier's formatting decisions. Prettier handles formatting; ESLint handles code quality.
Per-directory overrides
Flat config makes per-directory rules easy:
export default tseslint.config(
// ... base config ...
{
files: ['src/**/*.test.ts', 'src/**/*.spec.ts'],
rules: {
'@typescript-eslint/no-explicit-any': 'off', // more permissive in tests
'no-console': 'off',
},
},
{
files: ['scripts/**/*.js'],
rules: {
'no-console': 'off', // scripts can log freely
},
},
);
CI integration
# .github/workflows/lint.yml
name: Lint
on: [push, pull_request]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'npm'
- run: npm ci
- run: npm run lint
- run: npm run format:check
How to pick your rule set
- Greenfield TypeScript project? Start with
tseslint.configs.recommendedTypeChecked plus eslint-config-prettier. Add rules from tseslint.configs.strictTypeChecked as the team's comfort grows.
- Migrating a large JS codebase? Start with
js.configs.recommended only, fix violations, then layer in TypeScript rules incrementally.
- React project? Add
eslint-plugin-react, eslint-plugin-react-hooks, and eslint-plugin-jsx-a11y.
- Node.js backend? Add
eslint-plugin-n for Node-specific rules (deprecated API detection, import resolution).
- Speed matters above all? Try Biome as a Rust-based alternative — 10–20× faster, single config for lint and format.
Common mistakes
Using .eslintrc with ESLint 9+. The legacy config format requires the compatibility shim (@eslint/eslintrc) and will be removed in a future version. Migrate now.
no-explicit-any: error on a legacy codebase. This will produce hundreds of errors and block adoption. Start with warn, fix over time, promote to error once clean.
Not running ESLint in CI. Editor-only linting means violations reach main whenever someone commits outside the editor or with plugins disabled.
Linting generated code. Auto-generated files (*.d.ts, codegen output) should be in ignores — linting them adds noise and slows runs.
Different ESLint versions across team members. Lock ESLint in devDependencies and run through npm ci in CI, not global installs.
What to skip
eslint-plugin-prettier — it runs Prettier as an ESLint rule, which is slower and conflates two concerns. Use them separately.
- Every
@eslint/recommended rule without reading what each does — some rules are too strict for your codebase and generate noise that desensitises the team.
- Custom rules for style that Prettier already handles — indent, quotes, semicolons are Prettier's job.
FAQ
How do I migrate from .eslintrc to flat config?
Run npx @eslint/migrate-config .eslintrc.json. It generates an eslint.config.mjs from your existing config. Review it manually — some plugins have not released flat-config-compatible versions yet.
Should I use recommendedTypeChecked or just recommended?
Use recommendedTypeChecked — it enables rules that require type information (like no-floating-promises) and catches a class of bugs that type-unaware rules miss entirely.
Is Biome ready to replace ESLint?
For most projects, Biome is production-ready and dramatically faster. The gap is plugin ecosystem — ESLint has hundreds of framework-specific plugins that Biome does not yet match. Evaluate based on whether you need those plugins.
How do I share an ESLint config across multiple packages in a monorepo?
Create a shared eslint-config-<yourname> package in your monorepo and reference it from each package's eslint.config.js. Flat config makes this simpler than the legacy extends approach.
Where to go next
See How to reduce bundle size in 2026, How to write unit tests in 2026, and How to set up CI/CD in 2026.