【ESLint】個人テンプレートのESLintを改善するためにeslint-config-airbnbを調査してみる
モチベーション
- 個人テンプレートの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-react
とeslint-plugin-jsx-a11y
も入ります。
"extends": ["airbnb"]
公式でも記載されているようにReact周りの設定が不要な場合は以下のように指定しeslint-config-airbnb-base
のみ読み込むようにします。
"extends": ["airbnb-base"]
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-airbnb
がeslint-plugin-react
, eslint-plugin-react-hooks
, eslint-plugin-jsx-a11y
のどのようなルールを設定をしているかは下記から確認することができます。
airbnb/hooksとreact-hooks/recommendedの設定を比較してみる
airbnb/hooks
とreact-hooks/recommended
の設定を比較してどのような違いがあるか見ていきましょう。
airbnb/hooksの場合
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'error',
react-hooks/recommendedの場合
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'warn',
上記のように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",
typescript-airbnb
一方でtypescript-airbnb
ではデフォルト設定で@typescript-eslint/no-shadow
が有効化されているのでrulesに追記しなくてもエラーが出ます。
全ての項目を比較したわけではありませんがairbnbの方が元のリンター等のおすすめ設定より厳しめにしているのでしょうか?気が向いたら深掘っていきたいです。
@typescript-eslint/〇〇のrulesをアップデートさせる
ここまで理解した上で@typescript-eslint/〇〇に絞って選定をしていきます。
非nullアサーション演算子(Non-Null Assertion Operator)をブロックする
nullチェックすれば良いのを妥協していたので削除。
// 以下を削除
"@typescript-eslint/no-non-null-assertion": "off"
Error オブジェクトの代わりに数値や文字列、boolean などのリテラルを投げるのをブロックする
そもそもErrorオブジェクト以外を投げようとしていた背景が不明だったので削除。
// 以下を削除
"@typescript-eslint/no-throw-literal": "off"
戻り値に明示的に型をつけない関数をブロックする
業務委託先で導入されているので真似て導入していたが思った以上に: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"
}
}
]
}
外部スコープで宣言された変数と同じ名前の変数を宣言することブロックする
再帰的な処理でもない限りは変数名を被らせない方が良いと思ったので削除。
// 以下を削除
"@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と被るのでエラーが発生する
明示的なany型の利用をブロックする
any絶対許さないマン。
と言いながら時には必要な場合もあるのでerrorで設定するのではなく、rulesから削除しデフォルトのwarnを適用させる。
// 以下を削除
"@typescript-eslint/no-explicit-any": "off",
結果を変数に格納しない三項演算子をブロックする
// 以下のように変更
"@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)
}
}
最後に
ちょっと長くなりましたが一旦これで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"
}
}
参考記事
Discussion