Adopting ESLint’s Flat Config for Cleaner, Programmable Linting
Learn how ESLint v8’s flat configuration replaces .eslintrc files with a single JavaScript file, enabling overrides, TypeScript support, and easier sharing.
29 May 2026, 20:03 UTC

The problem: juggling multiple .eslintrc files
Many projects grow a collection of .eslintrc* files—one for source code, another for tests, maybe a third for CI. Over time these files drift, duplicate rules, and become hard to reason about because ESLint merges them silently. Changing a rule often means hunting through several places to see where it actually takes effect.
Thesis: flat configuration puts lint control in JavaScript
ESLint v8 introduced a flat config system where you write a single eslint.config.js (or eslint.config.cjs) that exports an array of config objects. Each object can target specific globs, apply plugins, set parser options, and define rules. Because the config is plain JavaScript, you can compute values, import shared settings, and even use TypeScript for intellisense via the defineConfig helper.
Understanding the flat config shape
A flat config file returns an array where each entry looks like this:
{
files: ['src/**/*.js'],
languageOptions: { ecmaVersion: 2022, sourceType: 'module' },
plugins: [pluginImport],
rules: { 'import/order': ['error', { 'newlines-between': 'always' }] }
}
The files field uses minimatch patterns to limit the config’s scope. If omitted, the config applies to all files processed by ESLint. Multiple entries are merged in order, later entries overriding earlier ones—just like the old cascading behavior, but now visible in one place.
Worked example: separate rules for source and tests
Suppose you want no-console warned in production code but allowed in test files. With flat config you can express this in a few lines:
// eslint.config.js
import { defineConfig } from '@eslint/config-array';
const base = {
languageOptions: { ecmaVersion: 2022, sourceType: 'module' },
rules: {
semi: ['error', 'always'],
'no-console': 'warn'
}
};
const testOverride = {
files: ['**/__tests__/**/*.js', '**/*.test.js'],
rules: { 'no-console': 'off' }
};
export default defineConfig([base, testOverride]);
To run the linter on your project:
- Ensure you have ESLint v8 or newer:
npm i -D eslint@^8. - If you use TypeScript in the config, install the helper:
npm i -D @eslint/config-array. - Place the file above at the project root.
- Execute:
npx eslint . --ext .js.
You should see warnings for console statements in src/ but none in test files. To verify the resolved configuration for a specific file, run:
npx eslint --print-config src/app.js
The output JSON shows the merged rules, confirming that no-console is set to warn for that path.
Trade‑offs and limitations
- Version requirement: Flat config only works with ESLint ≥ 8.0.0. Older projects must upgrade or keep using
.eslintrc*. - Syntax errors are fatal: Because the config is executed as JavaScript, a typo or missing import will cause ESLint to exit with an unclear message. Lint the config file itself (e.g.,
npx eslint eslint.config.js) to catch problems early. - Learning curve: Teams accustomed to the declarative
.eslintrcJSON/YAML may need time to adjust to programmatic config.
Practical way to check that your flat config is working: after running ESLint, compare the output of --print-config for a file you expect to be affected versus one you expect to be ignored. Differences in the rules section confirm that overrides are applied as intended.
Actionable closing
If you’re starting a new project or planning an upgrade, adopt flat config now:
- Install ESLint ≥ 8 and
@eslint/config-arrayif you want TypeScript support. - Create
eslint.config.jsusing the pattern shown above. - Migrate existing
.eslintrc*files withnpx @eslint/migrate-config(review the output). - Validate with
--print-configand run your usual lint script. - Add a lint‑config check to your CI pipeline to prevent silent failures.
By moving to a single, programmable config file you gain visibility, flexibility, and a smoother path for sharing lint settings across packages—without the mystery of hidden merges.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.