📌

【ESLint】個人テンプレートのESLintを改善するためにeslint-config-airbnbを調査してみる

2023/03/31に公開

モチベーション

  • 個人テンプレートのESLintをアップデートさせる
  • 〇〇/recommendedなどおすすめルールセットではなくairbnbのルールセットをベースにしたい
  • airbnbでどのようなルールセットが適応されているのか探すのが面倒なので設定ファイルのリンクを載せて辞書っぽい記事にしたい
改善前のESLint
{
  "env": {
    "browser": true,
    "es2021": true
  },
  "extends": [
    "eslint:recommended",
    "plugin:@typescript-eslint/recommended",
    "plugin:react/recommended",
    "plugin:react-hooks/recommended",
    "airbnb",
    "airbnb-typescript",
    "prettier"
  ],
  "parser": "@typescript-eslint/parser",
  "parserOptions": {
    "ecmaFeatures": {
      "jsx": true
    },
    "ecmaVersion": "latest",
    "sourceType": "module",
    "project": "./tsconfig.json"
  },
  "plugins": ["react", "react-hooks", "@typescript-eslint"],
  "rules": {
    "react-hooks/rules-of-hooks": "error",
    "react-hooks/exhaustive-deps": "error",
    "@typescript-eslint/no-non-null-assertion": "off",
    "@typescript-eslint/no-explicit-any": "off",
    "@typescript-eslint/no-throw-literal": "off",
    "@typescript-eslint/explicit-module-boundary-types": "off",
    "react/require-default-props": "off",
    "spaced-comment": "off",
    "no-console": "off",
    "no-alert": "off",
    "arrow-body-style": "off",
    "import/prefer-default-export": "off",
    "@typescript-eslint/no-shadow": "off",
    "@typescript-eslint/naming-convention": "off",
    "@typescript-eslint/no-unused-expressions": "off",
    "@typescript-eslint/no-unused-vars": "off",
    "react/prop-types": "off",
    "react/jsx-props-no-spreading": "off",
    "react/react-in-jsx-scope": "off",
    "react/function-component-definition": [
      2,
      { "namedComponents": "arrow-function" }
    ]
  }
}

eslint-config-airbnb

eslint-config-airbnbをちゃんと使いこなしたい(ベースにしたい)ので改めてREADMEを読んでみる。

私たちのデフォルトのエクスポートには、ECMAScript 6+とReactを含む、私たちのESLintルールのほとんどが含まれています。eslint、eslint-plugin-import、eslint-plugin-react、eslint-plugin-react-hooks、eslint-plugin-jsx-a11yを必要とします。

ちなみに下記のようにextendsにairbnbを追加した場合はeslint-plugin-reacteslint-plugin-jsx-a11yも入ります。

"extends": ["airbnb"]

https://github.com/airbnb/javascript/blob/master/packages/eslint-config-airbnb/index.js#L1-L3

公式でも記載されているようにReact周りの設定が不要な場合は以下のように指定しeslint-config-airbnb-baseのみ読み込むようにします。

"extends": ["airbnb-base"]

https://github.com/airbnb/javascript/blob/5c01a1094986c4dd50a6ee4d9f7617abdfabb58a/packages/eslint-config-airbnb-base/index.js#L1-L3

eslint-plugin-react-hooksを有効にするには別途extendsに追加するようにREADMEに記載されています。

なお、React Hooksのルールは有効になりません。これらを有効にするには、eslint-config-airbnb/hooksセクションを参照してください。

このエントリポイントでは、Reactフックのlintingルールを有効にします(v16.8+が必要です)。使用するには、"extends "を追加します:.eslintrc に ["airbnb", "airbnb/hooks"] を追加します。

"extends": ["airbnb", "airbnb/hooks"]

eslint-config-airbnbeslint-plugin-react, eslint-plugin-react-hooks, eslint-plugin-jsx-a11yのどのようなルールを設定をしているかは下記から確認することができます。

https://github.com/airbnb/javascript/tree/master/packages/eslint-config-airbnb/rules#L1-L3

airbnb/hooksとreact-hooks/recommendedの設定を比較してみる

airbnb/hooksreact-hooks/recommendedの設定を比較してどのような違いがあるか見ていきましょう。

airbnb/hooksの場合

'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'error',

https://github.com/airbnb/javascript/blob/master/packages/eslint-config-airbnb/rules/react-hooks.js#L1-L3

react-hooks/recommendedの場合

'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'warn',

https://github.com/facebook/react/blob/main/packages/eslint-plugin-react-hooks/src/index.js#L1-L3

上記のようにairbnb/hooksでは'react-hooks/exhaustive-deps': 'error'になっていますが、react-hooks/recommendedでは'react-hooks/exhaustive-deps': 'warn'として設定されています。

airbnb/hooksの方が厳しめに設定しているのですね。

個人的にはreact-hooks/exhaustive-depsをerrorにするのは厳しすぎるかなと思うので、以下の選択肢が存在するかと思います。

  • airbnb/hooksだけを導入し、rulesで'react-hooks/exhaustive-deps': 'warn'として設定を上書きする。
  • react-hooks/recommendedだけを導入しデフォルト設定を受け入れる。

個人的にはベースをairbnbで揃えたい気持ちがあったので前者を選択しました。

typescript-airbnbと@typescript-eslint/recommendedの設定を比較してみる

具体的には@typescript-eslint/no-shadowにスポットを当てて設定内容を比較していこうと思います。

@typescript-eslint/recommended

@typescript-eslint/no-shadow@typescript-eslint/recommendedではデフォルト設定していないようなので特にワーニングやエラーが起きることはありません。(公式ドキュメントと実際のコードを両方とも確認済み)

もし@typescript-eslint/no-shadowを有効させるには別途rulesに設定を追加する必要があります。

// 以下をrulesに追加
"@typescript-eslint/no-shadow": "error",

https://typescript-eslint.io/rules/

https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/src/configs/recommended.ts#L1-L3

typescript-airbnb

一方でtypescript-airbnbではデフォルト設定で@typescript-eslint/no-shadowが有効化されているのでrulesに追記しなくてもエラーが出ます。

全ての項目を比較したわけではありませんがairbnbの方が元のリンター等のおすすめ設定より厳しめにしているのでしょうか?気が向いたら深掘っていきたいです。

https://github.com/iamturns/eslint-config-airbnb-typescript/blob/master/lib/shared.js#L1-L3

https://github.com/airbnb/javascript/blob/master/packages/eslint-config-airbnb-base/rules/variables.js#L1-L3

@typescript-eslint/〇〇のrulesをアップデートさせる

ここまで理解した上で@typescript-eslint/〇〇に絞って選定をしていきます。

非nullアサーション演算子(Non-Null Assertion Operator)をブロックする

nullチェックすれば良いのを妥協していたので削除。

// 以下を削除
"@typescript-eslint/no-non-null-assertion": "off"

https://typescript-eslint.io/rules/no-non-null-assertion/

Error オブジェクトの代わりに数値や文字列、boolean などのリテラルを投げるのをブロックする

そもそもErrorオブジェクト以外を投げようとしていた背景が不明だったので削除。

// 以下を削除
"@typescript-eslint/no-throw-literal": "off"

https://typescript-eslint.io/rules/no-throw-literal/

戻り値に明示的に型をつけない関数をブロックする

業務委託先で導入されているので真似て導入していたが思った以上に:void祭りになっていて旨味を感じなかったので削除。

jsxファイルにも適用するには別途overridesする必要がある。

// 以下を削除
"@typescript-eslint/explicit-module-boundary-types": "error"
// overridesの具体例を公式から引っ張ってきた
{
  "rules": {
    // disable the rule for all files
    "@typescript-eslint/explicit-module-boundary-types": "off"
  },
  "overrides": [
    {
      // enable the rule specifically for TypeScript files
      "files": ["*.ts", "*.mts", "*.cts", "*.tsx"],
      "rules": {
        "@typescript-eslint/explicit-module-boundary-types": "error"
      }
    }
  ]
}

https://typescript-eslint.io/rules/explicit-module-boundary-types/

外部スコープで宣言された変数と同じ名前の変数を宣言することブロックする

再帰的な処理でもない限りは変数名を被らせない方が良いと思ったので削除。

// 以下を削除
"@typescript-eslint/no-shadow": "off",
// エラーが発生する具体例
const {
    register,
    setValue,
    formState: { errors },
    watch,
    handleSubmit
  } = useForm<FormData>({
    resolver: yupResolver(getSchema())
  })

const onError: SubmitErrorHandler<FormData> = (errors) => {}
// 引数の変数(errors)がreact-hook-formのerrorsと被るのでエラーが発生する

https://typescript-eslint.io/rules/no-shadow

明示的なany型の利用をブロックする

any絶対許さないマン。

と言いながら時には必要な場合もあるのでerrorで設定するのではなく、rulesから削除しデフォルトのwarnを適用させる。

// 以下を削除
"@typescript-eslint/no-explicit-any": "off",

https://typescript-eslint.io/rules/no-explicit-any

結果を変数に格納しない三項演算子をブロックする

// 以下のように変更
"@typescript-eslint/no-unused-expressions": [
      "error",
      { "allowTernary": true }
    ], 

具体的な使用例としてはbool値によって関数の発火を出しわけしたいときno-unused-expressionsが有効(error)になっていると不便だったので変更。

普通にif文で分ければno-unused-expressionsを有効(error)でも問題ないので好みの問題かもしれません。

// no-unused-expressionsが有効(error)だと下記がエラーになる
const handleFavorite = (isFavorite: boolean, id: number) => {
      try {
        isFavorite
          ? removeFavoriteMutation.mutate(id)
          : addFavoriteMutation.mutate(id)
      } catch (error) {
        handleError(error)
      }
    }

https://typescript-eslint.io/rules/no-unused-expressions

最後に

ちょっと長くなりましたが一旦これでFIXさせようと思います。
また更新するかもしれません。

もし間違い等ありましたらご指摘いただけますと幸いです。
最後までご覧いただきありがとうございました。

改善後のESLint
{
  "env": {
    "browser": true,
    "es2021": true
  },
  "extends": ["airbnb", "airbnb/hooks", "airbnb-typescript", "prettier"],
  "parserOptions": {
    "project": "./tsconfig.json"
  },
  "rules": {
    // eslint-config-airbnb(eslint)
    "spaced-comment": "off",
    "no-console": "off",
    "no-alert": "off",
    "arrow-body-style": "off",
    // eslint-config-airbnb(eslint-plugin-import)
    "import/prefer-default-export": "off",
    // eslint-config-airbnb(eslint-plugin-react)
    "react/require-default-props": "off",
    "react/prop-types": "off",
    "react/jsx-props-no-spreading": "off",
    "react/react-in-jsx-scope": "off",
    "react/function-component-definition": [
      2,
      { "namedComponents": "arrow-function" }
    ],
    // eslint-config-airbnb(eslint-plugin-react-hook)
    "react-hooks/exhaustive-deps": "warn",
    // eslint-config-airbnb-typescript(eslint-typescript)
    "@typescript-eslint/naming-convention": "off",
    "@typescript-eslint/no-unused-expressions": [
      "error",
      { "allowTernary": true }
    ],
    "@typescript-eslint/no-unused-vars": "off"
  }
}

参考記事

https://zenn.dev/yoshiko/articles/0994f518015c04

https://zenn.dev/kimromi/articles/b7cf98005f3193

https://uit-inside.linecorp.com/episode/105

Discussion