🐕

個人的にいいなと思った Eslint, Prettier, VSCode設定

2024/04/04に公開

はじめに

今回のコードはこちら↓
https://github.com/49takaya3989/personal_eslint_prettier_vscode_settings

できる限りサンプルコードを記載するようにはしているが、最後らへんはめんどくさくなって省いているところも多いです。お許しください。

必要なパッケージ

npm install --save-dev eslint eslint-config-prettier eslint-plugin-unicorn eslint-plugin-functional prettier-plugin-organize-imports eslint-plugin-import eslint-plugin-react-hooks eslint-plugin-react eslint-plugin-prefer-arrow-functions eslint-import-resolver-typescript

ESLint

env

各環境下で定義されている global 変数を ESlint 実行環境で使用可能にするための設定。

browser: true,
es2020: true,
node: true,

parserOptions

sourceType: 'module'
モジュールとしてコードを解析する設定。
project: ['tsconfig.json']
@typescript-eslint/parser に必要な TypeScript の方情報にアクセスするための設定。

settings

react: { version: 'detect' }
プロジェクトで使用している React のバージョンに適した規則が適用される。

reportUnusedDisableDirectives

eslint-disable の不要になったコメントを削除する。

rules

非 null アサーション演算子!の利用を禁止する。

const test = hoge.fuga!.fizz
// Forbidden non-null assertion.eslint@typescript-eslint/no-non-null-assertion

命名規則を強制する。

Boolean型が期待される式においてBoolean型以外の型使用を禁止する。
個人的には厳しめの設定↓が好み

"@typescript-eslint/strict-boolean-expressions": [
  "error",
  {
    "allowString": false,
    "allowNumber": false,
    "allowNullableObject": false,
    "allowNullableBoolean": false,
    "allowNullableString": false,
    "allowNullableNumber": false,
    "allowAny": false,
  }
],
const hoge: string = "hoge"if (test) console.log('hoge');
// Unexpected nullable string value in conditional. Please handle the nullish/empty cases explicitly.eslint@typescript-eslint/strict-boolean-expressionsif (test != null) console.log('hoge');

JSX で&&の左辺をBoolean型に強制したり、三項演算子に強制したりする。
validStrategiesにはcoerceternary2つのオプションがあり、coerceは前者、ternaryは後者の強制をになっている。オプションは1つ以上設定する必要あり。
@typescript-eslint/strict-boolean-expressionsと組み合わせて使用する場合は、ternaryのみの指定となる。

const test1: string = ""
const test2: string = "test2"

return (
  <>{test1 && test2}
{/*  Potential leaked value that might cause unintentionally rendered values or rendering crasheseslintreact/jsx-no-leaked-render */}{test1 != null ? test2 : null}
  </>
)

+foo!fooの型変換の短い表記法を禁止する。

const num = "1"const num2 = +num
use `Number(num)` instead.eslintno-implicit-coercion

✅ const num2 = Number(num)

文字列の結合に+を使用することを禁止する。

const test2: string = "test2"
return (
  <>{'hoge' + test2}
{/*  Unexpected string concatenation.eslintprefer-template */}{`hoge${test2}`}
  </>
)

eslint . --fixを実行すると{'hoge${ test2}'}となるので気持ち悪い人は修正する必要がある。

異なる型の加算を禁止する。
個人的には厳しめの設定↓が好み

"@typescript-eslint/restrict-plus-operands": [
  "error",
  {
    "allowAny": true,
    "allowBoolean": true,
    "allowNullish": true,
    "allowNumberAndString": true,
    "allowRegExp": true,
    "skipCompoundAssignments": false,
  }
],
const str: string = "1"
const num: number = 1const sum =  num + str
// Operands of '+' operations must be a number or string. Got `number` + `string`.eslint@typescript-eslint/restrict-plus-operands

switch 文の網羅性を強制する。

"@typescript-eslint/switch-exhaustiveness-check": [
  "error",
  {
    allowDefaultCaseForExhaustiveSwitch: true,
    requireDefaultForNonUnion: true,
  },
],

allowDefaultCaseForExhaustiveSwitch: true
全てのケースが網羅されている switch 文で default ケースを許容する。
requireDefaultForNonUnion: true
ユニオン型でない値の switch 文で default ケースを強制する。

type DayName = 'Monday' | 'Tuesday';

type Day = {
  [key: string]: DayName
}

const day: Day = {
  mon: 'Monday',
  tue: 'Tuesday',
};switch(day.key) {
  case "Monday":
    console.log('hoge');
    break;
}
// Switch is not exhaustive. Cases not matched: "Wednesday" | "Thursday" | "Friday" | "Saturday" | "Sunday"eslint@typescript-eslint/switch-exhaustiveness-checkswitch(day.key) {
  case "Monday":
    console.log('hoge');
    break;
  case "Tuesday":
    console.log('hoge1');
    break;
}switch(day.key) {
  case "Monday":
    console.log('hoge');
    break;
  default:
    break;
}switch(day.key) {
  case "Monday":
    console.log('hoge');
    break;
  case "Tuesday":
    console.log('hoge1');
    break;
  default:
    break;
}const handler = (num: number) => {
  switch(num) {
    case 1:
      console.log('hoge');
      break;
    }
  }
}
// Switch is not exhaustive. Cases not matched: defaulteslint@typescript-eslint/switch-exhaustiveness-checkconst handler = (num: number) => {
  switch(num) {
    case 1:
      console.log('hoge');
      break;
    default:
      break;
  }
}

if 文の網羅性を禁止し、 switch 文に強制する。

"unicorn/prefer-switch": [
  "error",
  {
    "minimumCases": 2,
    "emptyDefaultCase": "do-nothing-comment",
  }
],

"minimumCases": 2
if {} else {}の形のみ許容する。
"emptyDefaultCase": "do-nothing-comment"
elseがないif文から整形する際にdefaultを出力する

switch文のfallthrough(breakやreturnを行わないこと)を禁止する。
tsconfig にもnoFallthroughCasesInSwitchという同じような設定が存在し、今回はReact(TypeScript)のプロジェクトに入れることを前提として考えているので、noFallthroughCasesInSwitchを使用する。

既存の定義されているグローバル変数の使用を制限する。
一旦今回はこんな感じ↓

"no-restricted-globals": [
  "error",
  {
    "name": "event",
    "message": "Use local parameter instead. Global 'event' object can lead to unintended behavior."
  },
  {
    "name": "isNaN",
    "message": "Use Number.isNaN instead to avoid type coercion."
  },
  {
    "name": "isFinite",
    "message": "Use Number.isFinite instead to avoid type coercion."
  },
],

必要になれば都度追加していく。

console.log(isNaN(num));
// Unexpected use of 'isNaN'. Use Number.isNaN instead to avoid type coercion.eslintno-restricted-globalsconsole.log(isFinite(num));
Unexpected use of 'isFinite'. Use Number.isFinite instead to avoid type coercion.eslintno-restricted-globals
❌ const onClick = () => {
     console.log(event);
   }
// Unexpected use of 'event'. Use local parameter instead. Global 'event' object can lead to unintended behavior.eslintno-restricted-globalsconsole.log(Number.isNaN(num));console.log(Number.isFinite(num));const onClick = (event) => {
     console.log(event);
   }

"functional/no-let": [
  "error",
  {
    "allowInForLoopInit": true,
  }
]

"allowInForLoopInit": true
for (let i = 0; i < array.length; i++) {}みたいに、for 文の引数では許容する。

let hoge = 1;
const t = () => {let hoge2 = 1
}
// Unexpected let, use const instead.eslintfunctional/no-letconst hoge3 = 1;for( let i = 0; i < 5; i++) {}
'functional/immutable-data': [
  'error',
  {
    ignoreClasses: true,
    ignoreImmediateMutation: true,
    ignoreAccessorPattern: [
      '**.current.**',
      '**.scrollTop',
    ],
  },
],

ignoreClasses: true,
Classes 内の変更は許容する。
ignoreImmediateMutation: true,
オブジェクトを変数に代入する前の変更は許容する。
ignoreAccessorPattern: ['**.current.**', '**.scrollTop',],
ref, scrollTop の変更は許容する。

const obj = { foo: 1 }
❌ obj.bar = 2;
// Modifying an existing object/array is not allowed.eslintfunctional/immutable-data
"@typescript-eslint/consistent-type-imports": [
  "error",
  {
    "prefer": 'type-imports',
    "disallowTypeAnnotations": false,
    "fixStyle": 'inline-type-imports',
  }
],

"prefer": 'type-imports'
type の import では {type 〇〇} を強制する。
"disallowTypeAnnotations": false
型注釈での import を禁止する。
"fixStyle": 'inline-type-imports'
type を inline にする。

"@typescript-eslint/consistent-type-exports": [
  "error",
  {
    "fixMixedExportsWithInlineTypeSpecifier": true,
  }
],

"fixMixedExportsWithInlineTypeSpecifier": true
関数や変数と型をまとめて export する際に、 inline で書くことを許容する。

"@typescript-eslint/require-array-sort-compare": [
  "error",
  {
    "ignoreStringArrays": true,
  }
],

"ignoreStringArrays": true
文字列の配列では許容する。

  • react/jsx-no-bind
    無駄なレンダリングが起こり得るコードを禁止する。
    今回は下記↓の設定。
"react/jsx-no-bind": [
  "error",
  {
    "allowArrowFunctions": true,
  }
],

"allowArrowFunctions": true
何でもかんでも useCallback 使えばいいというのは嫌いなので、アロー関数のみ有効にする。

  • eslint-plugin-react-hooks
    plugin:react-hooks/recommendedでも warn 設定がされているが、 error に強制する。
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "error",

"react-hooks/rules-of-hooks": "error"
Hooks の正しいルールを強制する。
"react-hooks/exhaustive-deps"
deps を強制する。

"import/order": [
  "error",
  {
    groups: [
      "builtin",
      "external",
      "internal",
      "parent",
      "sibling",
      "index",
      "object",
      "type",
    ],
    pathGroups: [
      {
        pattern: "{react,react-dom/**,react-router-dom}",
        group: "external",
        position: "before",
      },
      {
        pattern: "~/*",
        group: "internal",
        position: "before",
      },
    ],
    pathGroupsExcludedImportTypes: ["builtin"],
    alphabetize: {
      order: "asc",
      caseInsensitive: true,
    },
    "newlines-between": "always",
  },
],

groups: ["builtin", "external", "internal", "parent", "sibling", "index", "object", "type"],
import をカテゴリ分けする。
builtin:Node.jsの組み込みモジュール(例: fs, pathなど)
external:外部からインストールされたモジュール(例: react, lodashなどのnpmパッケージ)
internal:プロジェクト内で定義されたエイリアス(例: ~/foo/bar)
parent:親ディレクトリ(例: ../)
sibling:同一ディレクトリ内のファイル(例: ./foo)
index:同一ディレクトリ内のindexファイル(例: ./)
object:動的インポート(例: import log = console.log;)
type:TypeScriptの型定義(例: import type { Foo } from 'foo';)
pathGroups: [{ pattern: "{react,react-dom/**,react-router-dom}", group: "external", position: "before" }, { pattern: "react-dom/**", group: "external", position: "before" }, { pattern: "react-router-dom", group: "external", position: "before" }]
path ごとにグループ化する。
pattern:指定したいパスの文字列。 minimatch パターンでかける。
group:pattern で指定した対象の属するグループを指定する。
position:import する位置を group で指定した対象の相対位置で指定する。
pathGroupsExcludedImportTypes: ["builtin"]
pathGroups の設定から除外する group を指定する。
alphabetize: { order: "asc" }
昇順に並び替える。
alphabetize: { caseInsensitive: true }
大文字小文字をの区別を無視する。
"newlines-between": "always"
group 間の改行を強制する。
参考記事
https://zenn.dev/kazukix/articles/eslint-config-react-native
https://zenn.dev/noshiro_piko/articles/take-full-advantage-of-typescript-eslint

Prettier

みんなにわかりやすいコードを目指す上で Prettier はデフォルトの設定を用いるのが一番いいと思われるので、特にカスタマイズはしない。
デフォルトの設定は下記↓

/** @type {import("prettier").Config} */
const config = {
  printWidth: 80, // 折り返し位置
  tabWidth: 2, // インデントサイズ
  useTabs: false, // タブでインデント
  semi: true, // 文末のセミコロン
  singleQuote: false, // シングルクウォートの使用
  quoteProps: "as-needed", // オブジェクトプロパティのクォート
  jsxSingleQuote: false, // JSX でのシングルクウォートの使用
  trailingComma: "all", // 末尾のカンマ
  bracketSpacing: true, // オブジェクト内の要素と括弧の間のスペース
  bracketSameLine: false, // HTML タグの閉じ括弧を同一行に
  arrowParens: "always", // アロー関数の引数括弧
  rangeStart: 0, // フォーマット適用開始位置
  rangeEnd: Infinity, // フォーマット適用完了位置
  requirePragma: false, // プラグマを含むファイルのみにフォーマット適用
  insertPragma: false, // プラグマの挿入
  proseWrap: "preserve", // マークダウンテキストのテキスト折り返し
  htmlWhitespaceSensitivity: "css", // 空白感度の指定
  endOfLine: "lf", // 改行コードの指定
  embeddedLanguageFormatting: "auto", // 組み込み言語のフォーマット
  singleAttributePerLine: false, // HTML, JSX で1行1属性
};

module.exports = config;

https://zenn.dev/rescuenow/articles/c07dd571dfe10f

VSCode

VSCode もさまざまなカスタマイズがあるが、 ESLint と Prettier を活かす最低限の設定にする。

{
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": "always" // ファイル保存時にESLintによる自動修正を実行する。
  },
  "eslint.validate": [
    "javascript",
    "javascriptreact",
    "typescript",
    "typescriptreact"
  ], // ESLintがチェックを行うファイルタイプを指定する。
  "typescript.tsdk": "node_modules/typescript/lib", // プロジェクトで使用されているTypeScriptのバージョンをVS Codeに指定する。
  "editor.formatOnSave": true, // ファイル保存時にフォーマットを自動で実行する。
  "prettier.enable": true // Prettierを有効にする。
}

(おまけ)エイリアスの設定

tsconfig.json と vite.config.ts それぞれに下記設定を追記する。

tsconfig.json
"compilerOptions": {
  ・・・
  "baseUrl": "./",
  "paths": {
    "~/*": ["./src/*"]
  }
},
vite.config.ts
export default defineConfig({
  ・・・
  resolve: {
    alias: {
      "~/": path.resolve(__dirname, "./src"),
    },
  },
});

以上!

Discussion