Flat Config for ESLint 9: Stop Fighting .eslintrc Overrides in Monorepos
ESLint 9 flat config replaces .eslintrc cascade with an explicit eslint.config.js array. Learn the migration pattern for TypeScript, explicit imports, and the performance and memory trade-offs to plan for.
20 Apr 2026, 11:26 UTC

The problem is not that ESLint is slow. The problem is that .eslintrc cascade makes it hard to reason about which rules actually apply to a file. In a TypeScript monorepo with apps, packages and tests, overrides nest, extends merge unpredictably, and a rule change in the root silently changes behavior three directories down. ESLint 9 flat config replaces that cascade with an explicit array of config objects in a single eslint.config.js. The useful takeaway: you get per-file targeting without filesystem walks, but you must import plugins and parsers explicitly and audit plugin ESM support before migrating.
Why flat config changes the decision model
ESLint 9.0 released April 2024 makes flat config the default. Instead of .eslintrc.json, .eslintrc.js and cascading directories, you export an array from eslint.config.js, eslint.config.mjs or eslint.config.cjs. Each object can declare files via globs and is applied in order. The final rule set for a file is the merge of all matching configs.
This removes the implicit walk up the directory tree. Config is loaded once at startup, then a combined rule set is built per file. That is why benchmarks report 10-30% faster startup on large monorepos, at the cost of a heavier initial parse. Caching with eslint --cache remains essential.
Key structural changes to internalize:
- languageOptions replaces parserOptions, ecmaVersion, sourceType and globals. Globals are no longer provided by env shorthand.
- Plugins and parsers are imported, not named strings. Dependencies become visible to the package manager.
- Auto-fix runs in a single pass per file across all matching configs. This reduces surprise fixes but means a later config cannot depend on a fixer from an earlier config running again.
Migration pattern for TypeScript projects
ESLint 9 drops Node 16/17. Confirm the runtime first in the project root:
node -v
Required: Node 18.18+ for ESLint 9. Risk: running on older Node produces an engine error at install time.
Check installed ESLint version:
npx eslint --version
Expected check: output starts with 9.x. If not, upgrade before changing config.
For TypeScript, the consolidated typescript-eslint package v8+ replaces @typescript-eslint/parser and @typescript-eslint/eslint-plugin. Type-aware rules now use languageOptions.projectService instead of parserOptions.project.
Minimal working flat config for a mixed JS/TS repo:
// eslint.config.js
import js from '@eslint/js';
import globals from 'globals';
import tseslint from 'typescript-eslint';
export default [
{
files: ['**/*.{js,mjs,cjs}'],
languageOptions: {
ecmaVersion: 2022,
sourceType: 'module',
globals: { ...globals.browser, ...globals.node }
},
rules: { ...js.configs.recommended.rules }
},
...tseslint.configs.recommended,
{
files: ['**/*.ts','**/*.tsx'],
languageOptions: {
parser: tseslint.parser,
projectService: true,
tsconfigRootDir: import.meta.dirname
}
}
];
Run in project root with read access to source files:
npx eslint .
Expected checks: config loads without ReferenceError for globals, and lint output appears for .ts files. Risk: forgetting to add globals as a devDependency causes ReferenceError at config load time.
Migration tooling can generate a first draft:
npx @eslint/migrate-config .eslintrc.json
Run on a copy of the repo. The tool does not rewrite plugin imports or handle custom rule implementations. Manual review of generated files is required, especially for overrides with complex excludedFiles logic.
Trade-offs and limitations to plan for
Explicit imports make dependencies auditable, but require all plugins to publish ESM-compatible entry points. As of late 2024 not all community plugins support flat config. Check each plugin changelog for flat config support before migrating.
projectService: true enables type-aware rules but increases memory usage on large codebases. Limit scope with targeted files globs and tsconfigRootDir. Without a valid tsconfig.json, type-aware rules are silent or error.
IDE integration is automatic with VS Code ESLint extension v3.0+. Older extensions may need eslint.experimental.useFlatConfig: true. Verify by opening a file with a known lint error and confirming Quick Fix works.
Custom formatters and processors need updates. The Processor interface changed to accept flat config file pattern objects; old preprocess/postprocess signatures are incompatible.
Actionable adoption checklist
Keep a backup of existing .eslintrc files before changing state. Mixing flat config and .eslintrc in one repo is unsupported.
- Confirm Node 18.18+ and ESLint 9.x.
- Install devDependencies: typescript-eslint@8, globals, @eslint/js.
- Create eslint.config.js with explicit imports and languageOptions.
- Run eslint . on a small path to verify config loads.
- Test migration on a copy of the repo and diff overrides.
- Benchmark with time npx eslint . --cache before and after on the full codebase.
Flat config does not magically fix rule quality. It makes the rule application model explicit, which is the engineering win for teams maintaining multiple packages.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.