ESLint Overrides: Apply Different Rules to Tests, TypeScript, and Source Files
Use ESLint's overrides to apply different rule sets to tests, TypeScript, and source files from one config — with correct glob patterns, precedence order, and verification via --print-config.
28 Sept 2026, 16:15 UTC

When one project contains source code, test files, and config scripts, a single global rule set rarely fits all of them. ESLint's overrides feature solves this: you keep one configuration file and apply different rules to different file groups, such as relaxing no-console in scripts or enabling TypeScript-specific rules only for .ts files. The key behavior to remember is that overrides are matched in order, and later entries win when a file matches more than one pattern.
How overrides work
In the legacy .eslintrc format, overrides is an array of objects at the top level of your config. Each object has a files array of glob patterns plus any normal config keys (rules, parser, env, extends, plugins). When ESLint lints a file, it merges every matching override on top of the base config, in array order. Later matching entries take precedence over earlier ones for conflicting keys.
This replaces the older pattern of dropping separate .eslintrc files into subdirectories, which made the effective rules hard to trace from one place.
A worked example
Here is a legacy-format config that enforces strict TypeScript rules for source files while relaxing rules for tests. Save it as .eslintrc.cjs in your project root (use .cjs if your package has "type": "module"):
module.exports = {
root: true,
parserOptions: { ecmaVersion: 2022, sourceType: 'module' },
env: { node: true },
rules: {
'no-unused-vars': 'error',
'no-console': 'warn'
},
overrides: [
{
files: ['**/*.ts', '**/*.tsx'],
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint'],
extends: ['plugin:@typescript-eslint/recommended'],
rules: {
'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': 'error'
}
},
{
files: ['**/*.test.js', '**/*.test.ts', 'tests/**'],
env: { jest: true },
rules: {
'no-console': 'off'
}
}
]
};Two details matter here. First, the TypeScript override disables the core no-unused-vars rule and enables the @typescript-eslint replacement; leaving both on produces duplicate or false reports for type-only constructs. Second, a file like src/util.test.ts matches both overrides. Because the test override comes later, its no-console: 'off' wins for that file, while the TypeScript parser and rules still apply since the test override does not touch them.
Flat config is different
In ESLint v9's flat config (eslint.config.js), there is no overrides key at all. Instead, every entry in the exported array can carry its own files patterns, and the array order plays the same precedence role:
const tseslint = require('typescript-eslint');
module.exports = [
{
files: ['**/*.{js,ts}'],
rules: { 'no-console': 'warn' }
},
...tseslint.configs.recommended.map(cfg => ({
...cfg,
files: ['**/*.ts']
})),
{
files: ['**/*.test.{js,ts}'],
rules: { 'no-console': 'off' }
}
];The concept is identical — scoped rules, later entries win — but the syntax is not interchangeable. Check which format your ESLint version uses (npx eslint --version; v9 defaults to flat config) before copying examples from older blog posts.
Common mistakes
- Glob surprises. Patterns in
filesare matched relative to the config file location. Writing*.test.jsmatches only top-level files; you almost always want**/*.test.js. An overly broad pattern like**/*.jsin a late override can silently re-apply rules you thought were scoped. - Wrong precedence assumptions. If two overrides both set the same rule, the last matching one wins. Put the most specific overrides last, or debug surprises will follow.
- Forgetting the parser swap. Enabling
@typescript-eslintrules without settingparser: '@typescript-eslint/parser'in the same override causes parse errors or rules that never fire. - Deep nesting. Overrides inside
extends-ed shared configs stack with your own. The merged result can be hard to reason about without tooling.
Verifying the effective config
Never trust the glob by reading alone. ESLint ships a diagnostic flag that prints the fully resolved configuration for one file. Run this from your project root (no special permissions needed):
npx eslint --print-config src/util.test.tsThe command outputs the merged JSON config — parser, plugins, env, and every rule with its final severity — exactly as ESLint will apply it. Check that the rule you scoped is present (or absent) as intended. Note that in flat config mode, --print-config was removed in favor of the config inspector (npx @eslint/config-inspector); verify which tool your version supports.
A second, behavioral check: create a scratch file matching the override pattern, deliberately violate a rule that exists only in that override (for example, an unused variable under the TypeScript override), and run npx eslint path/to/scratch.ts. If the expected error appears, the override is active. Delete the scratch file afterward.
Limits
Overrides merge configuration; they do not create isolated lint contexts. Plugins and parsers loaded in one override are still registered for the run, and a file matching no override simply gets the base config. Also, overrides cannot be nested inside other overrides in the legacy format, and performance can suffer if you define dozens of overlapping patterns, since every file is tested against every entry. Keep the array short, order it from general to specific, and verify with --print-config whenever you change it.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.