Featured image of post ESLint Flat Config: Migration Guide from Legacy .eslintrc

ESLint Flat Config: Migration Guide from Legacy .eslintrc

Complete guide to migrating from legacy .eslintrc to ESLint flat config system. Covers new format, plugin compatibility, TypeScript integration, Prettier setup, and performance gains.

ESLint v9.0, released in 2024, made the flat config system (eslint.config.js) the default, replacing the legacy .eslintrc format that had been in use for over a decade. This migration guide covers the complete transition process, new concepts, and practical examples.

Why Flat Config?

The legacy .eslintrc system had several problems: configuration cascading made behavior unpredictable, directory-based resolution was complex, JSON/YAML lacked support for functions and comments, and shareable configs were difficult to compose. Flat config addresses these with a single configuration file, explicit composition via JavaScript arrays, simplified plugin resolution, and full JavaScript-native syntax.

The Flat Config Format

The new format exports an array of config objects from eslint.config.js:

import js from "@eslint/js";
import typescript from "typescript-eslint";

export default [
  js.configs.recommended,
  ...typescript.configs.recommended,
  {
    files: ["src/**/*.ts"],
    rules: {
      "@typescript-eslint/no-unused-vars": "error",
    },
  },
  {
    ignores: ["dist/**", "node_modules/**"],
  },
];

Each config object can specify files (which files to apply to), ignores (which to exclude), rules, plugins, languageOptions, and linterOptions. The ignores key replaces .eslintignore.


Migration Steps

Converting an existing .eslintrc project follows a straightforward pattern:

Legacy ConceptFlat Config Equivalent
parserlanguageOptions.parser
parserOptionslanguageOptions.parserOptions
envlanguageOptions.globals
extendsConfig array composition
plugins (strings)Imported plugin objects
overridesMultiple config objects with files
.eslintignoreignores in config objects
// Before: .eslintrc.json
// { "env": { "browser": true }, "extends": ["eslint:recommended"] }

// After: eslint.config.js
import globals from "globals";
import js from "@eslint/js";

export default [
  js.configs.recommended,
  {
    languageOptions: { globals: globals.browser },
  },
];

TypeScript Integration

typescript-eslint v7+ provides first-class flat config support:

import tseslint from "typescript-eslint";

export default tseslint.config({
  files: ["src/**/*.ts"],
  languageOptions: {
    parserOptions: { project: true },
  },
  rules: {
    "@typescript-eslint/no-floating-promises": "error",
  },
});

The tseslint.config() helper provides type checking for config objects and simplifies TypeScript-aware linting setup.


Plugin Compatibility

Plugins in flat config are imported as objects, not referenced as strings:

import react from "eslint-plugin-react";

export default [
  {
    files: ["src/**/*.jsx"],
    plugins: { react },
    rules: {
      "react/jsx-uses-react": "error",
    },
  },
];

For legacy plugins not yet updated for flat config, use @eslint/compat’s fixupPluginRules wrapper. The FlatCompat utility helps bridge legacy extends chains during incremental migration.

Performance Improvements

Flat config achieves O(1) config resolution compared to the legacy O(n) cascade system. For large monorepos, this translates to significantly faster linting. Additional caching improvements in ESLint v9 further reduce lint times in CI pipelines.


CI Configuration

# .github/workflows/lint.yml
jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
      - run: npm ci
      - run: npx eslint src/ --cache

Common Migration Pitfalls

Watch for missing @eslint/js preset (it provides eslint:recommended), confusion around env replacement (use globals packages), and empty config objects that apply to all files. Use --debug flag when troubleshooting: npx eslint --debug src/.

Conclusion

Migrating to flat config simplifies ESLint setup, improves performance, and provides a more maintainable configuration format. The initial migration effort pays dividends through clearer config composition, faster linting, and better tooling integration. Start with a single project, validate the output matches your legacy config, and then roll out across your codebase.