typescript-eslintとFlat Config

typescript-eslintのFlat Configについて、自分に今必要そうな部分だけをひととおり確認したので忘れる前にメモを残しておく。

前提

  • 素のJavaScriptプロジェクトをやることは自分はあまりなさそうなのでTypeScript前提
  • ES Modules前提でいいやと思っているので設定ファイルの拡張子はシンプルに .js にする
  • フォーマッターにはESLintのStylisticじゃなくてESLint外のフォーマッター(PrettierやBiome)を使う前提

基本の設定

https://typescript-eslint.io/getting-started/ の最初に書いてある設定。

// @ts-check

import eslint from '@eslint/js';
import tseslint from 'typescript-eslint';

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

この設定の意味

  • **/*.js, **/*.cjs, and **/*.mjs'**/*.ts', '**/*.tsx', '**/*.mts', '**/*.cts' に対して
  • eslint.configs.recommendedtseslint.configs.recommended が適用される

シンプルだね。

参照:推奨ルールの実装

tseslint.config() って何?

ESLintのドキュメントでは配列で定義しているのに、typescript-eslintのドキュメントでは tseslint.config() を使っている。どうしてだろう?って思って調べた。

↓ESLintでは配列で定義している。https://eslint.org/docs/latest/use/configure/configuration-files より。

export default [
    {
        rules: {
            semi: "error",
            "prefer-const": "error"
        }
    }
];

↓こちらの記事を参考にして追いかけた。ありがたい。

次の2点だった。

  • 設定を型安全に書けるようになる
  • extends を使えるように拡張されている

tseslint.config() は設定の配列を返す単純なラッパーになっている。なので tseslint.config() を使わなくても設定は書ける。でも、これを使うと簡単に型安全な設定を書ける。

だから tseslint.config() は使おうと思う。

一方で、これは個人的な好みの問題だけど extends はできれば使いたくないな。typescript-eslint独自の拡張だから。

参照:tseslint.config() の実装

... がついてる例とついてない例を見かけるよね?

さっきはこういう設定↓を書いたけど

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

いろいろ調べてると、こんな設定の例↓を見かけるときもある。配列が展開されている。

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

これはどちらでもいい。... をつけなくても tseslint.config() が配列を展開してくれる。僕は前者の ... をつけない方で書こうかな。

適用対象のファイルがどう決まっているのか?

さて、さっき↓こういう設定に対して

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

**/*.js, **/*.cjs, and **/*.mjs'**/*.ts', '**/*.tsx', '**/*.mts', '**/*.cts' が適用対象になる」と言ったがこれはどう決まっているのか?

Flat Configでのfilesの扱い

Flat Configは設定の配列になっている。そして、この中に定義している files によって対象となるファイルが決まる。

  • デフォルトの対象ファイルは **/*.js, **/*.cjs, and **/*.mjs
  • files が定義されていない設定の場合
    • デフォルトの対象ファイルに適用される
    • それに加えて他にfilesを指定している設定があれば、そのファイルにも適用される
  • files が定義されている設定の場合
    • 指定されたファイルのみに適用される
  • ファイルが複数の設定にマッチする場合
    • 設定がマージされて適用される。下に定義してある設定によって上書きされる形でマージされる

参照

By default, ESLint lints files that match the patterns **/*.js, **/*.cjs, and **/*.mjs https://eslint.org/docs/latest/use/configure/configuration-files#specifying-files-and-ignores

展開して見てみよう

さきほどの設定のrecommendedを展開するとこういう感じになる。

export default [
  // eslint.configs.recommendedを展開したもの
  // (1)
  {
    rules: {
      // ESLintの推奨ルール群
    }
  },

  // tseslint.configs.recommendedを展開したもの
  // (2)
  {
    name: 'typescript-eslint/base',
    // '@typescript-eslint'の有効化とパーサーの設定
    languageOptions: {
      parser,
      sourceType: 'module',
    },
    plugins: {
      '@typescript-eslint': plugin,
    },
  },
  // (3)
  {
    name: 'typescript-eslint/eslint-recommended',
    files: ['**/*.ts', '**/*.tsx', '**/*.mts', '**/*.cts'],
    rules: {
      // ESLint推奨ルール群の中で
      // TypeScriptがチェックしてくれるものはオフにしている
      // またTypeScriptに合わせてオンにしたほうがいいものはオンにしている
    }
  },
  // (4)
  {
    name: 'typescript-eslint/recommended',
    rules: {
      // typescript-eslintの推奨ルール群
    }
  }
];

(1)(2)(4)には files が定義されていない。(3)にだけ files が定義されている。つまり次のようになる。

  • (1)(2)(4)
    • ESLintデフォルトの **/*.js, **/*.cjs, and **/*.mjs に適用される
    • (3)のファイルも適用対象となるので '**/*.ts', '**/*.tsx', '**/*.mts', '**/*.cts' にも適用される
  • (3)
    • files で指定された '**/*.ts', '**/*.tsx', '**/*.mts', '**/*.cts' だけに適用される

重ね合わせを考えると

  • JSのファイルの場合→(1)(2)(4)の重ね合わせになる
  • TSのファイルの場合→(1)(2)(3)(4)の重ね合わせになる

(3)ではESLintの推奨ルール群をTypeScript用に上書きしているので、その上書きはTSのファイルに対してのみ有効になっている。

ちなみに(2)でTypeScript用のプラグインとパーサーが指定してあるのでTSファイルを解析できている。

tseslint.configs.recommendedeslint.configs.recommended のルールの一部をTypeScriptに合わせて上書きしている部分は eslint-recommended-raw.ts に定義されている。

TypeScriptの型情報が必要なルール

ここまで tseslint.configs.recommended を見てきたが、TypeScriptの型情報が必要なルールは別のセットになっている。なので、それを適用したい場合は recommendedTypeChecked を使う。

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

recommendedTypeChecked には tseslint.configs.recommended の内容も含まれているので両方を書くのではなく recommendedTypeChecked だけを書けばいい。

ただ、これだけでは正しく動作しない。パーサーに型情報を生成するように設定しないといけない。次のように書く。

export default tseslint.config(
  eslint.configs.recommended,
  tseslint.configs.recommendedTypeChecked,
  {
    languageOptions: {
      parserOptions: {
        projectService: true,
        tsconfigRootDir: import.meta.dirname,
      },
    },
  },
);

これで型情報が必要なルールも適用される。ふつうは型情報を必要とするルールを適用したいだろうと思うので、recommended よりも recommendedTypeChecked をメインで使いそう。

別セットに分かれている理由は、デフォルトの推奨ルールを速くするためらしい。ふむふむ。

導入を容易にするため、デフォルトのルールを速くするため、とのこと。

typescript-eslintのrecommendedって2種類あんねん

参照:recommendedTypeCheckedの実装

除外したいファイル

Flat Configの設定で ignores を指定すると設定の対象から除外できる。デフォルトでは "**/node_modules/", ".git/" が除外対象になっている。

This pattern is added after the default patterns, which are ["**/node_modules/", ".git/"]. https://eslint.org/docs/latest/use/configure/ignore#ignoring-files

ignores が単独で使用されると特別な意味を持つことに注意

  • ignores が単独で使用されると、すべての設定に適用される。
  • ignores が他の項目と一緒に使用されると、その設定のみに除外設定が適用される。
export default [
    {
        ignores: [".config/*"]
    }
];

この場合 "**/node_modules/", ".git/" に加えて .config ディレクトリがすべての設定から除外される。

例えばビルド出力用のフォルダを dist にしている場合は、↓こんな風に定義しておくと良さそう。

export default tseslint.config(
  {
    ignores: ["dist/"],
  },
  eslint.configs.recommended,
  tseslint.configs.recommendedTypeChecked,
  {
    languageOptions: {
      parserOptions: {
        projectService: true,
        tsconfigRootDir: import.meta.dirname,
      },
    },
  },
);

フォーマッタについて

最後にフォーマッタについて書いておく。ファイルのフォーマットにPrettierを使うにしてもBiomeを使うにしても、ESLintのフォーマットまわりのルールをオフにしておきたいので eslint-config-prettier を設定の最後に追加しておく。

// @ts-check

import eslint from '@eslint/js';
import prettierConfig from 'eslint-config-prettier';
import tseslint from 'typescript-eslint';

export default tseslint.config(
  {
    ignores: ["dist/"],
  },
  eslint.configs.recommended,
  tseslint.configs.recommendedTypeChecked,
  {
    languageOptions: {
      parserOptions: {
        projectService: true,
        tsconfigRootDir: import.meta.dirname,
      },
    },
  },
  prettierConfig, // 途中の設定がONにしていてもOFFになるように、最後に追加しておく
);

こんなところかな。これで最近調べたことをいったん忘れても大丈夫そう。よかった。

参照

ESLintのドキュメント

typescript-eslintのドキュメント

参考にした記事