ESLint 9 and Flat Config: Complete Migration and Configuration Guide
Hey HaWkers, ESLint 9 brought a significant change in how we configure the linter: Flat Config. If you're still using .eslintrc.js or .eslintrc.json, it's time to understand the new system and migrate your projects.
In this guide we'll cover everything from basic concepts to advanced configurations for TypeScript and React projects.
Why Flat Config?
The old ESLint configuration system had some limitations:
Problems with the old system:
- Confusing configuration cascade
- Complex plugin resolution
- Difficulty understanding which rule comes from where
- Performance affected by configuration file searching
Benefits of Flat Config:
- Single configuration file (
eslint.config.js) - Explicit imports of plugins and configurations
- Better performance
- Easier to debug and understand
- Native ESM support
Basic Flat Config Structure
Flat Config uses an eslint.config.js (or .mjs, .cjs) file in the project root:
// eslint.config.js
import js from '@eslint/js';
export default [
js.configs.recommended,
{
rules: {
'no-unused-vars': 'warn',
'no-console': 'off',
},
},
];Configuration Anatomy
// eslint.config.js
export default [
// Configuration 1: Applies to all files
{
rules: {
'semi': ['error', 'always'],
},
},
// Configuration 2: Applies only to specific files
{
files: ['**/*.ts', '**/*.tsx'],
rules: {
'@typescript-eslint/no-explicit-any': 'warn',
},
},
// Configuration 3: Ignores files
{
ignores: ['dist/**', 'node_modules/**', '*.config.js'],
},
];
Migration from Old System
Before: .eslintrc.js
// .eslintrc.js (old)
module.exports = {
env: {
browser: true,
es2021: true,
node: true,
},
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react/recommended',
'prettier',
],
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaFeatures: {
jsx: true,
},
ecmaVersion: 'latest',
sourceType: 'module',
},
plugins: ['@typescript-eslint', 'react', 'react-hooks'],
rules: {
'react/react-in-jsx-scope': 'off',
'@typescript-eslint/no-unused-vars': 'warn',
},
};After: eslint.config.js
// eslint.config.js (new)
import js from '@eslint/js';
import typescript from '@typescript-eslint/eslint-plugin';
import typescriptParser from '@typescript-eslint/parser';
import react from 'eslint-plugin-react';
import reactHooks from 'eslint-plugin-react-hooks';
import prettier from 'eslint-config-prettier';
import globals from 'globals';
export default [
js.configs.recommended,
// Global configuration
{
languageOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
globals: {
...globals.browser,
...globals.node,
...globals.es2021,
},
},
},
// TypeScript
{
files: ['**/*.ts', '**/*.tsx'],
languageOptions: {
parser: typescriptParser,
parserOptions: {
ecmaFeatures: { jsx: true },
},
},
plugins: {
'@typescript-eslint': typescript,
},
rules: {
...typescript.configs.recommended.rules,
'@typescript-eslint/no-unused-vars': 'warn',
},
},
// React
{
files: ['**/*.jsx', '**/*.tsx'],
plugins: {
react,
'react-hooks': reactHooks,
},
rules: {
...react.configs.recommended.rules,
...reactHooks.configs.recommended.rules,
'react/react-in-jsx-scope': 'off',
},
settings: {
react: {
version: 'detect',
},
},
},
// Prettier (must be last)
prettier,
// Ignores
{
ignores: ['dist/**', 'node_modules/**', '.next/**'],
},
];
Common Configurations
React + TypeScript Project
// eslint.config.js
import js from '@eslint/js';
import typescript from 'typescript-eslint';
import react from 'eslint-plugin-react';
import reactHooks from 'eslint-plugin-react-hooks';
import jsxA11y from 'eslint-plugin-jsx-a11y';
import globals from 'globals';
export default [
js.configs.recommended,
...typescript.configs.recommended,
{
files: ['**/*.{js,jsx,ts,tsx}'],
languageOptions: {
globals: {
...globals.browser,
...globals.es2021,
},
},
plugins: {
react,
'react-hooks': reactHooks,
'jsx-a11y': jsxA11y,
},
rules: {
// React
'react/prop-types': 'off',
'react/react-in-jsx-scope': 'off',
'react/jsx-uses-react': 'off',
// Hooks
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'warn',
// Accessibility
'jsx-a11y/alt-text': 'error',
'jsx-a11y/anchor-is-valid': 'warn',
// TypeScript
'@typescript-eslint/no-unused-vars': ['warn', {
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
}],
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/no-explicit-any': 'warn',
},
settings: {
react: { version: 'detect' },
},
},
{
ignores: ['dist/**', 'build/**', 'node_modules/**'],
},
];Node.js + TypeScript Project
// eslint.config.js
import js from '@eslint/js';
import typescript from 'typescript-eslint';
import node from 'eslint-plugin-n';
import globals from 'globals';
export default [
js.configs.recommended,
...typescript.configs.recommended,
{
files: ['**/*.{js,ts}'],
languageOptions: {
globals: {
...globals.node,
...globals.es2021,
},
},
plugins: {
n: node,
},
rules: {
// Node.js
'n/no-missing-import': 'off', // TypeScript handles this
'n/no-unsupported-features/es-syntax': 'off',
'n/no-process-exit': 'warn',
// TypeScript
'@typescript-eslint/no-unused-vars': ['error', {
argsIgnorePattern: '^_',
}],
'@typescript-eslint/explicit-function-return-type': ['warn', {
allowExpressions: true,
}],
// General
'no-console': ['warn', { allow: ['warn', 'error'] }],
'prefer-const': 'error',
},
},
{
ignores: ['dist/**', 'node_modules/**', '*.js'],
},
];
Plugins and Their New Configurations
@typescript-eslint
typescript-eslint v8+ has native Flat Config support:
import typescript from 'typescript-eslint';
export default [
// Uses typescript-eslint helper
...typescript.configs.recommended,
// Or stricter configuration
...typescript.configs.strict,
// Customizations
{
files: ['**/*.ts', '**/*.tsx'],
rules: {
'@typescript-eslint/consistent-type-imports': 'error',
'@typescript-eslint/no-floating-promises': 'error',
},
},
];eslint-plugin-import
import importPlugin from 'eslint-plugin-import';
export default [
{
plugins: {
import: importPlugin,
},
rules: {
'import/order': ['error', {
groups: [
'builtin',
'external',
'internal',
'parent',
'sibling',
'index',
],
'newlines-between': 'always',
alphabetize: { order: 'asc' },
}],
'import/no-duplicates': 'error',
'import/no-unresolved': 'error',
},
settings: {
'import/resolver': {
typescript: true,
node: true,
},
},
},
];
Migration Tips
1. Use the Migration Tool
ESLint offers a migration tool:
npx @eslint/migrate-config .eslintrc.js2. Check Plugin Compatibility
Not all plugins have been updated for Flat Config. Check the documentation for each one:
// Some plugins still use the old format
// Use the compatibility layer if needed
import { FlatCompat } from '@eslint/eslintrc';
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
});
export default [
// Modern plugins
js.configs.recommended,
// Legacy plugins via compat
...compat.extends('plugin:legacy-plugin/recommended'),
// Your rules
{
rules: {
// ...
},
},
];3. Remove Old Files
After migrating, remove old configuration files:
rm .eslintrc.js .eslintrc.json .eslintrc.yaml .eslintignoreFlat Config uses the ignores property in the configuration file itself.
4. Update Scripts
{
"scripts": {
"lint": "eslint .",
"lint:fix": "eslint . --fix"
}
}
Configuration Debugging
View Final Configuration
npx eslint --print-config src/index.tsCheck Which Files Are Linted
npx eslint . --debug 2>&1 | grep "Linting"Test Specific Rules
// eslint.config.js - temporarily
export default [
{
files: ['**/*.ts'],
rules: {
// Test only one rule
'no-console': 'error',
},
},
];Conclusion
ESLint 9's Flat Config represents a significant evolution in how we configure linting in JavaScript and TypeScript projects. Although migration requires some initial effort, the benefits of clarity, performance, and maintainability are worth it.
Final recommendations:
- Migrate new projects directly to Flat Config
- For existing projects, use the migration tool as a starting point
- Check plugin compatibility before migrating
- Take advantage to review and simplify your rules during migration
If you're interested in more tools to improve your code quality, I recommend checking out the article Debugging JavaScript: Advanced Techniques with DevTools which complements linting practices with debugging techniques.

