As a frontend developer who has spent years working with complex TypeScript codebases, I’ve learned that solid linting is essential for maintaining long-term code quality. With the release of ESLint v9 and its new flat configuration system, I knew it was time to upgrade — and face the inevitable challenges that come with major version changes.
In this guide, I’ll share my step-by-step experience migrating to ESLint v9 in a TypeScript project — including common roadblocks and how to solve them.
If you’re working with an older ESLint version and a legacy config, this post is for you. Together, we'll break down how to transition to the new flat config system, exploring both its benefits and pitfalls along the way.
➡️ Note: If you're starting a fresh project, I recommend checking out the official ESLint Quick Start Guide instead.
Ready? Let’s dive into the good, the bad, and everything in between when migrating to ESLint v9.
What's New in ESLint v9?: 🎉
ESLint v9 introduces some breaking changes in the tool's history. Here are the key changes you need to know:
The Flat Config Revolution 🚀
Old System: 🏛️
- Config Files: .eslintrc.* (JSON, YAML, JavaScript)
- Plugins: Referenced by name and auto-loaded
- Rules: Specified under a top-level rules object
- Cascading Config Discovery: Searched parent directories for config files
- Limited ESM Support
// .eslintrc.json
{
"extends": ["eslint:recommended"],
"rules": {
"no-unused-vars": "warn",
"no-undef": "warn"
}
}
New System: 🌟
- Config File:
eslint.config.js
oreslint.config.mjs
- Plugins: Explicitly imported and provided to the configuration
- Rules: Defined within each configuration object in an array
- File Targeting: Each config object can have a files property
- Single Config File: No automatic scanning of parent directories
- Full ESM Support: Directly import plugins and configs
- Processors: Specified directly in configuration objects
// eslint.config.mjs
import pluginJs from "@eslint/js";
export default [
pluginJs.configs.recommended,
{
rules: {
"no-unused-vars": "warn",
"no-undef": "warn"
}
}
];
Other Important Changes ⚡
- Node.js Requirements: ESLint v9 requires
Node.js ≥ 18.18.0
- Deprecated API Removals: Many deprecated APIs have been removed
- Performance Improvements: The new system is designed to be more efficient
- Plugin Architecture Changes: Plugins can now provide flat configs directly
This architectural overhaul offers better performance and more explicit configurations, but requires significant changes to existing setups.
The Migration Checklist âś…
Before diving into the migration process, here's a clear roadmap to guide you:
-
Update ESLint to v9:
- Upgrade ESLint to the latest version (v9) in your project. This is the foundation for all the improvements and changes you'll be making.
-
Generate a Starting Point with the configuration migrator tool:
- Use the ESLint migration tool to convert your existing
.eslintrc
configuration to the neweslint.config.mjs
format. This will give you a head start on the migration process.
- Use the ESLint migration tool to convert your existing
-
Update Plugins and Dependencies:
- Ensure all related plugins and dependencies are updated to versions compatible with ESLint v9. This step is crucial to avoid version compatibility issues.
-
Fully Migrate to Flat Config:
- Follow the new recommended configuration from the ESLint documentation and its related dependencies or plugins. This involves organizing your rules, plugins, and settings within the
eslint.config.js
file to fully align with the flat config system.
- Follow the new recommended configuration from the ESLint documentation and its related dependencies or plugins. This involves organizing your rules, plugins, and settings within the
-
Verify Everything Still Works:
- Test your project thoroughly to ensure that ESLint is functioning correctly with the new configuration. Be prepared for initial hiccups as you fine-tune the setup.
-
Fix Inevitable Errors:
- Address any errors or issues that arise during the migration process. This might involve tweaking rules, updating plugin configurations, or resolving conflicts.
Step 1: Initial Config migration ⬆️
Let's start with the official migration tool:
npx @eslint/migrate-config .eslintrc.js
This generates an eslint.config.mjs
file that looks promising but will likely betray you soon. It's like when the horror movie starts with a sunny day—you just know things are about to go terribly wrong.
Here's a snippet of what you might see:
import { fixupConfigRules, fixupPluginRules } from '@eslint/compat'
import examplePlugin from 'eslint-plugin-example'
// Many imports later...
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const compat = new FlatCompat({
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended,
allConfig: js.configs.all,
})
export default [
// Recommended configs
...fixupConfigRules(compat.extends('eslint:recommended')),
// Plugins
{
plugins: {
example: fixupPluginRules(examplePlugin),
},
},
// Custom config
{
rules: {
'no-unused-vars': 'warn',
'no-undef': 'warn',
},
},
]
Even the official ESLint migration guide acknowledges that this tool doesn’t work well for .eslintrc.js
files. Here’s a quote from their docs:
ℹ️ Important
The configuration migrator doesn’t yet work well for.eslintrc.js
files. If you are using.eslintrc.js
, the migration results in a config file that matches the evaluated output of your configuration and won’t include any functions, conditionals, or anything other than the raw data represented in your configuration.
So, while this tool provides a starting point, it's far from perfect. You'll need to manually refine the configuration to ensure it aligns with the new flat config system and follows the recommended practices from the ESLint documentation.
Step 2: The First Bump in the Road 🛣️
After eagerly running your existing ESLint script with the new configuration, you'll likely encounter several compatibility issues. This is especially true if you're using TypeScript. In my case, I faced what I call the "TypeScript Compatibility Crisis."
The Error ❌
You might see an error like this:
TypeError: Class extends value undefined is not a constructor or null
at Object.<anonymous> (.../node_modules/@typescript-eslint/experimental-utils/...)
This cryptic error essentially means: "Your typescript-eslint
packages are outdated and not compatible with ESLint v9." 🚨
Understanding the Issue 🤔
The error occurs because the @typescript-eslint packages you're using are not up-to-date with the latest ESLint version. ESLint v9 introduces significant changes, including a new flat configuration system, which older packages may not support. 🔄
First milestone: A Temporary Victory 🎉
After updating TypeScript and the related ESLint plugins, my ESLint setup finally started working again. Hooray! 🥳 But don't celebrate too soon—the journey hasn’t ended yet. In the upcoming sections, we'll deep dive into how to update your ESLint plugins and identify which plugins support the new flat config and ESLint v9. This will help you resolve compatibility issues and ensure a smooth migration. 🛠️
By being aware of these potential hurdles, you'll be better prepared to tackle the challenges ahead and successfully transition to ESLint v9.
Step 3: Fully Migrate to Flat Config 🌟
In the previous ESLint versions, we used to directly extend the recommended rules like this:
{
"extends": [
"eslint:recommended",
"plugin:react/recommended",
"plugin:react-hooks/recommended",
"plugin:import/recommended",
"plugin:import/typescript",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended",
"prettier"
]
// rest of the config
}
With the auto-migration tool, it uses two extra functions from two new packages: @eslint/compat
and @eslint/eslintrc
. This approach is not ideal for performance management as it might increase the size of the build. The updated config would look like this:
const config = [
...fixupConfigRules(
compat.extends(
'eslint:recommended',
'plugin:react/recommended',
'plugin:react-hooks/recommended',
'plugin:import/recommended',
'plugin:import/typescript',
'plugin:@typescript-eslint/eslint-recommended',
'plugin:@typescript-eslint/recommended',
'prettier'
)
),
// rest of the config
]
export default config
The Challenge 🚧
To properly migrate, you need to check each plugin's documentation to see if they provide native flat config support. The ESLint v9 Support Tracking Issue is an invaluable resource here.
The Solution 🛠️
For example, if a plugin supports flat config, you can replace compatibility layers with direct imports:
// Old approach
import { fixupPluginRules } from '@eslint/compat';
import react from 'eslint-plugin-react';
// ...
plugins: {
react: fixupPluginRules(react),
// ...
}
// New approach
import reactPlugin from 'eslint-plugin-react';
// ...
plugins: {
react,
// ...
}
And for recommended configurations:
// Old way with compatibility layer
...fixupConfigRules(compat.extends('plugin:react/recommended'))
// New way
import pluginJs from '@eslint/js';
import react from 'eslint-plugin-react';
import importPlugin from 'eslint-plugin-import';
// ...then in your config array:
pluginJs.configs.recommended,
react.configs.flat.recommended,
importPlugin.flatConfigs.typescript,
Clearing the Confusion: Plugins vs. Recommended Configs 🤔
Personally, I found the distinction between plugins and recommended configs confusing at first. Here’s a quick clarification:
- Plugins: These are additional rules and processors that extend ESLint's functionality.
- Recommended Configs: These are predefined sets of rules provided by plugins that you can extend in your configuration.
With the new flat config system, if a plugin supports flat config and provides a recommended config, you don't need to add it as a plugin separately. Everything is included in the exported module. ESLint maintains plugin support for backward compatibility, but if the plugin supports the new flat config, use their recommended flat config directly.
For more details on migrating ESLint plugins to support the new flat config, refer to the official ESLint documentation.
Step 4: Adding Base Configuration and Overrides 🏗️
Now that we've fully migrated to the flat config system, it's time to fine-tune your ESLint setup by adding base configurations and overrides. This step is crucial for ensuring that your linting rules are applied correctly and efficiently across your project.
Key Configuration Elements 🔑
-
Language Options:
- Define the language options such as ecmaVersion, sourceType, and parserOptions. These settings ensure that ESLint understands the syntax and features of your codebase.
-
Ignore Patterns:
- Specify files and directories that ESLint should ignore. This is particularly useful for excluding generated files, node_modules, build or other directories that don't need linting.
-
Files:
- In the new flat config system, the files property is mandatory. It specifies the files or patterns that ESLint should lint. This ensures that only relevant files are checked.
-
Common rules and Overrides:
- Define your common rules and any overrides needed for specific files or directories.
-
ESLint and Prettier Integration:
- If you're using
esling-config-prettier
, ensure that it is always the last item in the configuration array. This ensures that Prettier's rules take precedence and avoid conflicts with ESLint rules.
- If you're using
Example Configuration đź“ś
Here's an example of how your eslint.config.mjs might look with these elements included:
// eslint.config.mjs
import js from '@eslint/js'
import reactPlugin from 'eslint-plugin-react'
import importPlugin from 'eslint-plugin-import'
import prettierConfig from 'eslint-config-prettier'
import tseslint from 'typescript-eslint'
export default [
{
ignores: [
'**/build/*',
'**/node_modules/*',
'**/.next/*',
// other ignored patterns
],
},
{ files: ['**/*.{js,mjs,cjs,ts,jsx,tsx}'] },
pluginJs.configs.recommended,
pluginReact.configs.flat.recommended,
importPlugin.flatConfigs.recommended,
...tseslint.configs.recommended,
// Base Configuration
{
languageOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
globals: globals.browser,
parserOptions: { ecmaFeatures: { jsx: true } },
},
settings: {
react: {
version: 'detect',
},
'import/resolver': {
typescript: true,
alias: true,
},
},
// Rules overrides
rules: {
'react/jsx-uses-react': 'off',
'react/react-in-jsx-scope': 'off',
'react/no-unescaped-entities': 'off',
'react/prop-types': 'warn',
'import/named': 'warn',
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/no-unused-vars': [
'error',
{ args: 'none', caughtErrors: 'none' },
],
},
},
// Prettier needs to be last
prettierEslint,
]
Importance of Configuration Order 🔄
The order of configurations in the array is crucial:
- Ignore Patterns: Should be defined early to exclude unnecessary files from linting.
- Files: Must be specified to ensure ESLint knows which files to lint.
- Recommended Configs: Should come next to establish a base set of rules.
- Common rules and Overrides: Apply specific rules and overrides after the recommended configs.
- Prettier: Always place Prettier-related configurations last to ensure they override any conflicting ESLint rules.
By carefully structuring your configuration, you can ensure that ESLint operates efficiently and effectively, maintaining high code quality across your project. 🚀
Step 5: Dealing with New Linting Errors (Don't Panic!) 🚨
After successfully migrating your configuration, you might see a flood of new linting errors when you first run ESLint. Don't panic! This is normal and expected due to updated recommended rule sets in ESLint v9, changes in plugin rules, and previously unenforced rules now being active.
Common Categories of Errors đź“ś
Most errors are easy to fix and fall into common categories:
- Missing Return Types on Functions
- Unused Variables
- Import/Export Issues
- React Prop Validation Warnings
How to Handle Them 🛠️
- Review the Errors: Go through the list of new linting errors.
- Fix One by One: Address each error systematically.
- Update Your Code: Make the necessary changes to comply with the new rules.
- Test Thoroughly: Ensure your code still functions correctly after the fixes.
You're Ready to Go! 🎉
Once you've addressed these errors, your codebase will be cleaner and more compliant with the latest best practices. Congratulations! You've successfully migrated to ESLint v9.
Stay calm, tackle the errors methodically, and enjoy the benefits of improved code quality and maintainability.
Conclusion: The Light at the End of the ESLint Tunnel 🌟
Congratulations! You've made it through the great ESLint v9 migration adventure. It's been a journey filled with twists, turns, and a few bumps in the road, but you've emerged victorious. Your codebase is now cleaner, more efficient, and ready to face the future with confidence.
Remember, the key to a successful migration is patience, attention to detail, and a good sense of humor. As you've seen, even the official ESLint documentation acknowledges that the migration tool isn't perfect, but with a bit of elbow grease and some careful configuration, you can overcome any challenge.
So, raise a glass (or a coffee mug) to your achievement! You've tackled compatibility issues, updated plugins, and fine-tuned your configurations like a pro. Your journey doesn't end here, though. Keep an eye on the ESLint documentation and the ESLint v9 Support Tracking Issue for the latest updates and best practices
And if you ever find yourself feeling overwhelmed, just remember: even the best developers face linting errors. The important thing is to stay calm, tackle them one by one, and maybe, just maybe, enjoy the process. After all, every error fixed brings you one step closer to a well-maintained and error-free codebase.
Happy linting, and may your code always be error-free! 🎉🍻