ESLint の Flat Config を書く時に読んでほしい記事 (2025-03版)

この記事は以下の記事の改訂版です.

susisu.hatenablog.com

ESLint v9 から Flat Config がデフォルトの設定ファイルの形式となり, 徐々に対応しているプラグインも増えて移行が進みつつありますが, 実際に移行したプロジェクトを見ているとしばしば勘違いなどから誤った設定をしている事例を目にします. ということで, Flat Config を書くにあたっていくつか知っておいて欲しいことや, よく見かけるミスをまとめてみました.

この記事では網羅的な説明はしませんので, ESLint や typescript-eslint の公式ドキュメントを前提として, 副読本的に参照してください.

(New) 既製の eslint-config を使うのもおすすめ

現代における ESLint の設定の記述は,

  • JavaScript / TypeScript のような言語ごとの設定
  • CommonJS / ES Modules のモジュール形式ごとの設定
  • 各種プラグインごとの設定

のようにいくつもの考慮すべきことがあり (しかも掛け算で効いてくる), それなりに複雑な仕事になっています.

もし設定を自分で記述するのが面倒で, かつ対象のプロジェクトが典型的なものであれば, 既製の eslint-config パッケージを使うのもおすすめです. 例えば,

などなど.

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 する際にどのような設定が適用されるかは, 以下のようにして決定されます.

  1. そのファイルを対象に含めている config object を全て取り出す
  2. それらの config object の内容を先頭から順番にマージする
    • 同じ設定項目については後勝ちです. オブジェクトは良い感じにマージされます

(New) 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. かわりに以下のように対象 (filesignores) を同じくする 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",
    },
  },
  // ...
];

これはあまりに煩わしいので, ESLint 自体が defineConfig() というユーティリティを提供してくれています*2. このユーティリティには config object の列を渡すことができますが, これらの config object には追加で extends を渡すことができます. 結局は上記と同じ形に展開されますが, 冗長な記述が減るため非常に便利です.

import eslint from "@eslint/js";
import tseslint from "typescript-eslint";
import { defineConfig } from "eslint/config";

export default defineConfig(
  {
    files: ["**/*.ts"],
    extends: [
      // 継承元 (1)
      eslint.configs.recommended,
      // 継承元 (2) (Array だが spread (...) は不要)
      tseslint.configs.recommended,
    ],
    rules: {
      "no-console": "off",
    },
  },
  // ...
);

詳しい使い方は公式のドキュメントブログ記事を参照してください.

なお defineConfig() は ESLint v9.22.0 で追加されたため, それより前のバージョンを使っている場合は別途 @eslint/config-helpers をインストールする必要があります.

ファイルの種類ごとに設定を記述するのがおすすめ

typescript-eslint のドキュメントを読むと, 最初に以下のような記述が案内されています.

import eslint from ".";
import tseslint from "typescript-eslint";

export default tseslint.config(
  eslint.configs.recommended,
  tseslint.configs.recommended,
);

これは TypeScript のファイルのみを lint するのであれば良いのですが, 同時に JavaScript で書かれた設定ファイルも lint するなど, 複数の種類のファイルを lint の対象にしたい場合は, この記述はあまりおすすめしません.

というのも, 実は上記の設定では全てのファイルが TypeScript として構文解析され, lint されることになります*3. この設定で JavaScript ファイルを lint した場合, 一部のルール (extension rules と呼ばれる類のもの) が正しく設定されていない状態で lint されてしまったり, JavaScript としては不正な構文 (型注釈など) が通過してしまったりします.

個人的には, まずは以下のようにファイルの種類ごとに設定を行うところから始めるのが, やや設定は冗長になるものの無難であろうと考えています.

import eslint from "@eslint/js";
import tseslint from "typescript-eslint";
import { defineConfig } from "eslint/config";

export default defineConfig(
  // JavaScript に対する設定
  {
    files: ["**/*.{js,cjs,mjs}"],
    extends: [eslint.configs.recommended],
  },
  // TypeScript に対する設定
  {
    files: ["**/*.{ts,tsx,cts,mts}"],
    extends: [eslint.configs.recommended, tseslint.configs.recommended],
  },
);

自分で config object をマージする際の注意

defineConfig() のようなユーティリティを使わずに共有設定を使用する際のよくあるミスとして, 以下のような記述がされることがあります.

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",
    },
  },
];

あるいは素直に defineConfig()extends を使いましょう.

import eslint from "@eslint/js";
import tseslint from "typescript-eslint";
import { defineConfig } from "eslint/config";

export default defineConfig(
  {
    files: ["**/*.js"],
    extends: [eslint.configs.recommended],
    rules: {
      "no-console": "error",
    },
  },
);

eslint-config-prettier は最後に書いておくのがおすすめ

ESLint と Prettier を併用する場合 eslint-config-prettier を使うことがありますが, これは結局全種類のファイルに適用することになるものなので, Flat Config の配列の末尾に置いておくのが最もシンプルです.

import eslint from "@eslint/js";
import tseslint from "typescript-eslint";
import prettier from "eslint-config-prettier";
import { defineConfig } from "eslint/config";

export default defineConfig(
  {
    files: ["**/*.{js,cjs,mjs}"],
    extends: [eslint.configs.recommended],
  },
  {
    files: ["**/*.{ts,tsx,cts,mts}"],
    extends: [eslint.configs.recommended, tseslint.configs.recommended],
  },
  prettier,
);

とはいえ現在では Prettier と衝突するようなフォーマットに関するルールは ESLint や typescript-eslint からは除かれていっているため, eslint-config-prettier の必要性は少なくなってきているとは思います.

その他のよくあるミス

  • 拡張子が .js である全てのファイルを対象にする場合は files: ["**/*.js"] のように記述します
    • 旧設定ファイルで同様の記述をする際 (overrides 内) は files: ["*.js"] と書きましたが, Flat Config ではこれはルートディレクトリにある拡張子が .js のファイルという意味になり, 対象となるファイルが限られてしまいます
  • CommonJS ファイルに対して指定すべき languageOptions.sourceType"commonjs" です
    • 旧設定ファイルでは "script" でしたが, Flat Config ではこれは特にモジュールを規定しないスクリプトという意味になり, strict のようなルールが意図しない動作をすることがあります
  • @eslint/jsglobals といったパッケージを使用する場合は, 明示的にインストールして devDependencies に含めるようにしましょう
    • これをしないと, 偶然そこにあっただけの意図しないバージョンのものを使用してしまう可能性があります
  • (New) ESLint v9.10.0 以降は ESLint 本体に型定義が同梱されるようになったので, @types/eslint は使わないようにしましょう
    • 型定義が本体に同梱されるようになったことで @types/eslint は更新が止まって急速に古びていっており, しばしば互換性の問題が出ます

*1:継承のような入れ子構造を持たず, 一次元的な配列で記述されることから Flat Config と呼ばれています

*2:以前は typescript-eslint などのサードパーティが提供していましたが, files や ignores のマージに直感的でない挙動がありました

*3:そんなの知るかよって感じですよね. 私もそう思います. こういった共有設定を使うのは簡単ではありますが, 何をしてくれるか (あるいは何をしてくれないか) についての説明が書かれることは typescript-eslint に限らず少なくて, 正しい理解を得るためにはソースコードを読みに行く必要が生じることがしばしばあります