ESLint v9.0がリリースされ、flat config(eslint.config.js)がデフォルト設定形式になった。これまでの.eslintrc.jsはv9でもESLINT_USE_FLAT_CONFIG=false環境変数で使えるが、いずれ廃止される予定だ。
いざ移行しようとすると「プラグインが読み込めない」「ルールが効かない」「TypeScriptのパースエラー」など、じわじわとハマりポイントが出てくる。実際のプロジェクトで踏んだ5つの罠と対処法を解説する。
flat configの基本構造をおさらい
従来の.eslintrc.jsとflat configの最大の違いは、グローバルなextendsがなくなり、すべてを配列で組み合わせる点だ。
// 旧: .eslintrc.js
module.exports = {
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'],
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint'],
rules: {
'no-console': 'warn',
},
}
// 新: eslint.config.js
import js from '@eslint/js'
import tseslint from 'typescript-eslint'
export default [
js.configs.recommended,
...tseslint.configs.recommended,
{
rules: {
'no-console': 'warn',
},
},
]
一見シンプルだが、この構造の変化がいくつかの罠を生む。
罠1: eslint-plugin-xxx がflat configに未対応でエラーになる
症状
TypeError: Key "plugins": Key "react": Object was not normalized.
または
Error: Plugin "react" was already defined.
原因
flat configはプラグインオブジェクトに新しい正規化要件がある。古いプラグインはflat configが想定するオブジェクト構造に対応していない場合がある。特にeslint-plugin-reactはv7.34以前でこの問題が起きやすい。
対策
プラグインのバージョンを確認し、flat config対応バージョンにアップデートする。
npm ls eslint-plugin-react
# eslint-plugin-react@7.33.x → flat config未対応
# eslint-plugin-react@7.34.x以降 → 対応済み
対応バージョンがない場合は@eslint/compatのfixupPluginRulesでラップする:
import { fixupPluginRules } from '@eslint/compat'
import reactPlugin from 'eslint-plugin-react'
export default [
{
plugins: {
react: fixupPluginRules(reactPlugin),
},
rules: {
'react/jsx-uses-react': 'error',
},
},
]
罠2: ignoresの書き方が変わり、意図しないファイルがLint対象になる
症状
node_modulesやdistディレクトリがLint対象になり、大量のエラーが出る。または逆に、Lintしたいファイルが除外される。
原因
旧来の.eslintignoreはflat configでは無視される。また、ignoresをconfigオブジェクト内に書くか、単独オブジェクトとして書くかで挙動が異なる。
// NG: これはこのconfigオブジェクトのfilesにのみ適用される「ローカルignore」
export default [
{
files: ['src/**/*.ts'],
ignores: ['src/**/*.test.ts'], // このconfigブロック内だけで有効
rules: { /* ... */ },
},
]
// OK: グローバルignoreにするには、filesを持たない単独オブジェクトにする
export default [
{
ignores: ['dist/**', 'node_modules/**', '**/*.min.js'],
},
{
files: ['src/**/*.ts'],
rules: { /* ... */ },
},
]
対策
グローバルな除外設定は必ず**filesを持たない独立したオブジェクト**として配列の先頭に置く。
// eslint.config.js
import js from '@eslint/js'
import tseslint from 'typescript-eslint'
export default [
// グローバルignoreは必ず先頭に
{
ignores: [
'dist/**',
'build/**',
'coverage/**',
'**/*.d.ts',
],
},
js.configs.recommended,
...tseslint.configs.recommended,
{
files: ['src/**/*.{ts,tsx}'],
rules: {
'no-console': 'warn',
},
},
]
罠3: TypeScriptプロジェクトでparserOptions.projectの設定が別物になった
症状
@typescript-eslintの型情報を使うルール(@typescript-eslint/no-floating-promisesなど)を有効にすると:
Error: You have used a rule which requires type information, but don't have parserOptions set to generate type information for this file.
原因
flat configではparserOptionsはlanguageOptions.parserOptionsに移動した。旧来の書き方では型情報が渡されない。
// NG: 旧来の書き方(flat configでは無効)
export default [
{
parserOptions: { // ← ここに書いても効かない
project: './tsconfig.json',
},
},
]
対策
languageOptionsの下にparserとparserOptionsを移す:
import tseslint from 'typescript-eslint'
import tsParser from '@typescript-eslint/parser'
export default [
...tseslint.configs.recommendedTypeChecked,
{
files: ['src/**/*.{ts,tsx}'],
languageOptions: {
parser: tsParser,
parserOptions: {
project: './tsconfig.json',
tsconfigRootDir: import.meta.dirname, // Node.js 20.11+
},
},
},
]
import.meta.dirnameが使えない環境(Node.js 20.10以前)では:
import { fileURLToPath } from 'url'
import { dirname } from 'path'
const __dirname = dirname(fileURLToPath(import.meta.url))
export default [
{
languageOptions: {
parserOptions: {
project: './tsconfig.json',
tsconfigRootDir: __dirname,
},
},
},
]
罠4: eslint-config-prettierの適用順序でルールが効かなくなる
症状
Prettierと競合するESLintルールを無効化するためにeslint-config-prettierを入れたのに、indentやquotesなどのルールが依然として発火する。または逆に、意図したルールが無効化される。
原因
flat configではextendsの代わりに配列の順序でルールが上書きされる。eslint-config-prettierを途中に挟むと、後続の設定がPrettier無効化より優先されてしまう。
対策
eslint-config-prettierは必ず配列の最後に置く:
import js from '@eslint/js'
import tseslint from 'typescript-eslint'
import prettier from 'eslint-config-prettier'
export default [
js.configs.recommended,
...tseslint.configs.recommended,
{
// プロジェクト固有のルール
rules: {
'no-console': 'warn',
'@typescript-eslint/no-explicit-any': 'error',
},
},
prettier, // ← 必ず最後!Prettierと競合するルールをすべて上書き
]
罠5: eslint --initで生成されたconfigがv8形式でv9に通らない
症状
npm init @eslint/config@latestを実行して生成されたeslint.config.jsをそのまま使ったら動いたが、既存の.eslintrc.jsを手動でflat configに変換したら動かない。
Error [ERR_REQUIRE_ESM]: require() of ES Module eslint.config.js not supported.
または
SyntaxError: Unexpected token 'export'
原因
flat configはESモジュール形式で書く必要があるが、プロジェクトが"type": "commonjs"(または未設定)の場合、eslint.config.jsがCJSとして解釈される。
対策
2つの方法がある。
方法A: ファイル拡張子を.mjsにする
mv eslint.config.js eslint.config.mjs
方法B: package.jsonに"type": "module"を追加する
{
"type": "module"
}
ただし方法Bはプロジェクト全体のモジュール形式が変わるため、他のCJS形式のconfigファイル(jest.config.jsなど)も影響を受ける。CJSで書かれたファイルを.cjsにリネームする必要があるケースもある。
プロジェクト全体の移行推奨手順:
# 1. flat config移行ツールを使う(旧.eslintrcを自動変換)
npx @eslint/migrate-config .eslintrc.js
# 2. 生成された eslint.config.mjs を確認・調整
# 3. 旧設定ファイルを削除
rm .eslintrc.js .eslintignore
# 4. 動作確認
npx eslint src/ --debug 2>&1 | head -30
まとめ
ESLint v9 flat config移行でよくある罠をまとめると:
| 罠 | 原因 | 対策 |
|---|---|---|
| プラグインがエラー | 旧プラグインが未対応 | バージョンアップ or fixupPluginRules |
| ignoresが効かない | files付きオブジェクト内に書いた |
単独オブジェクトとして先頭に置く |
| 型情報ルールがエラー | parserOptionsの場所が違う |
languageOptions.parserOptionsに移動 |
| Prettierルールが残る | 適用順序が間違い | eslint-config-prettierを最後に |
| ESMエラー | CJSプロジェクトで.jsを使用 |
.mjsにリネーム |
移行時はnpx @eslint/migrate-configを使って自動変換し、そこからプラグインのバージョンやlanguageOptionsの設定を調整するのが最も確実なアプローチだ。
公式マイグレーションガイドも合わせて参照してほしい。
