ESLint v9 から Flat Config がデフォルトの設定ファイルの形式となり, 徐々に対応しているプラグインも増えて移行が進みつつありますが, 実際に移行したプロジェクトを見ているとしばしば勘違いなどから誤った設定をしている事例を目にします. ということで, Flat Config を書くにあたっていくつか知っておいて欲しいことや, よく見かけるミスをまとめてみました.
この記事では網羅的な説明はしませんので, ESLint や typescript-eslint の公式ドキュメントを前提として, 副読本的に参照してください.
- Getting Started with ESLint - ESLint - Pluggable JavaScript Linter
- Getting Started | typescript-eslint
Flat Config のしくみ
Flat Config は config object と呼ばれるオブジェクトの配列で記述されます.
/* eslint.config.js */ export default [ // ↓ これが config object { files: ["**/*.js"], rules: { "no-console": "error", "no-empty-function": "warn", }, }, // ↓ こっちも { files: ["**/*.test.js"], rules: { "no-empty-function": "off", }, }, // ... ];
各 config object は二つの部分からできています.
- 設定の対象を記述する部分 (
files
,ignores
)- 記述がなければ任意のファイルが対象となります
- 設定の内容を記述する部分 (
plugins
,languageOptions
,rules
など)
あるファイルを lint する際にどのような設定が適用されるかは, 以下のようにして決定されます.
- そのファイルを対象に含めている config object を全て取り出す
- それらの config object の内容を先頭から順番にマージする
- 同じ設定項目については後勝ちです. オブジェクトは良い感じにマージされます
typescript-eslint の提供するユーティリティを使うのがおすすめ
ESLint や typescript-eslint, 各種プラグインなどが提供する recommended のような共有設定を利用する場合, これまでの設定ファイルの形式では extends
による設定の継承が行われてきました.
module.exports = { overrides: [ { files: ["*.ts"], extends: [ // 継承元 (1) "eslint:recommended", // 継承元 (2) "plugin:@typescript-eslint/recommended", ], rules: { "no-console": "error", }, }, // ... ], };
ところが Flat Config では以前と異なり, config object が extends
を使った継承をサポートしていません*1.
かわりに以下のように対象 (files
や ignores
) を同じくする config object を順番に書き, 最終的にそれらがマージされた設定が使われることで, 継承と同等の効果を実現できます.
import eslint from "@eslint/js"; import tseslint from "typescript-eslint"; export default [ // 継承元 (1) { files: ["**/*.ts"], ...eslint.configs.recommended, }, // 継承元 (2) ...tseslint.configs.recommended.map(config => ({ files: ["**/*.ts"], ...config, })), { files: ["**/*.ts"], rules: { "no-console": "error", }, }, // ... ];
まあこれはあまりに煩わしいので, typescript-eslint が tseslint.config()
というユーティリティを提供してくれています.
このユーティリティには config object の列を渡すことができますが, これらの config object には追加で extends
を渡すことができます.
結局は上記と同じ形に展開されますが, 冗長な記述が減るため非常に便利です.
import eslint from "@eslint/js"; import tseslint from "typescript-eslint"; export default tseslint.config( { files: ["**/*.ts"], extends: [ // 継承元 (1) eslint.configs.recommended, // 継承元 (2) ...tseslint.configs.recommended, ], rules: { "no-console": "off", }, }, // ... );
ファイルの種類ごとに設定を記述するのがおすすめ
typescript-eslint のドキュメントを読むと, 最初に以下のような記述が案内されています.
import eslint from "@eslint/js"; import tseslint from "typescript-eslint"; export default tseslint.config( eslint.configs.recommended, ...tseslint.configs.recommended, );
これは TypeScript のファイルのみを lint するのであれば良いのですが, 同時に JavaScript で書かれた設定ファイルも lint するなど, 複数の種類のファイルを lint の対象にしたい場合は, この記述はあまりおすすめしません.
というのも, 実は上記の設定では全てのファイルが TypeScript として構文解析され, lint されることになります*2. これには一長一短あり, JavaScript ファイルに対しても型を使ったルールが使えるようになる一方で, JavaScript としては不正な構文 (型注釈など) が含まれていても lint を通過してしまいます.
2024-08-17 追記: typescript-eslint の一部のルールを有効化するにあたっては衝突する ESLint 組み込みのルールを無効化する必要があるのですが, typescript-eslint v8.1.0 時点で上記の設定を行った場合, このルールの無効化が .ts など TypeScript ファイルに対してのみ行われるようになっています. これでは JavaScript ファイルに対しては不正な設定となってしまうことから, 元より JavaScript ファイルを同時に lint することを想定していない (か意図せずそうなってしまっている) のだと思われます.
個人的には, まずは以下のようにファイルの種類ごとに設定を行うところから始めるのが, やや設定は複雑になるものの無難であろうと考えています.
import eslint from "@eslint/js"; import tseslint from "typescript-eslint"; export default tseslint.config( // .js に対する設定 { files: ["**/*.js"], extends: [eslint.configs.recommended], }, // .ts に対する設定 { files: ["**/*.ts"], extends: [eslint.configs.recommended, ...tseslint.configs.recommended], }, );
JavaScript ファイルに対しても型を使ったルールを使いたいといった場合は, 上記の事情を理解した上で設定をしましょう.
自分で config object をマージする際の注意
tseslint.config()
を使わずに共有設定を使用する際のよくあるミスとして, 以下のような記述がされることがあります.
import eslint from "@eslint/js"; export default [ { files: ["**/*.js"], ...eslint.configs.recommended, rules: { "no-console": "error", }, }, ];
ところがこれでは eslint.configs.recommended
に含まれる rules
を, 直後に記述された rules
で全て上書きしてしまう (この場合, no-console
のみが有効となり, eslint.configs.recommended
に含まれていたルールは全て無効になる) ため, 正しい記述ではありません.
正しくは以下のように別々の config object を記述して, ESLint に rules
をマージしてもらうようにします.
import eslint from "@eslint/js"; export default [ { files: ["**/*.js"], ...eslint.configs.recommended, }, { files: ["**/*.js"], rules: { "no-console": "error", }, }, ];
あるいは素直に tseslint.config()
と extends
を使いましょう.
import eslint from "@eslint/js"; import tseslint from "typescript-eslint"; export default tseslint.config( { files: ["**/*.js"], extends: [eslint.configs.recommended], rules: { "no-console": "error", }, }, );
eslint-config-prettier は最後に書いておくのがおすすめ
ESLint と Prettier を併用する場合 eslint-config-prettier を使うことがありますが, これはファイルの種類ごとの extends
の中ではなく, Flat Config の配列の末尾に置いておくのが最もシンプルです.
import eslint from "@eslint/js"; import tseslint from "typescript-eslint"; import prettier from "eslint-config-prettier"; export default tseslint.config( { files: ["**/*.js"], extends: [eslint.configs.recommended], }, { files: ["**/*.ts"], extends: [eslint.configs.recommended, ...tseslint.configs.recommended], }, prettier, );
とはいえ現在では Prettier と衝突するようなフォーマットに関するルールは ESLint や typescript-eslint からは除かれていっているため, eslint-config-prettier の必要性は少なくなってきているとは思います.
その他のよくあるミス
- 拡張子が
.js
である全てのファイルを対象にする場合はfiles: ["**/*.js"]
のように記述します - CommonJS ファイルに対して指定すべき
languageOptions.sourceType
は"commonjs"
です @eslint/js
やglobals
といったパッケージを使用する場合は, 明示的にインストールしてdevDependencies
に含めるようにしましょう- これをしないと, 偶然そこにあっただけの意図しないバージョンのものを使用してしまう可能性があります