ESLint のススメ(Biome を使わない場合)

に公開

この記事は、まず Biome と ESLint の違いをざっくり整理した上で、Biome を採用しない(できない)場合に ESLint を「まず動く最小構成」で導入し、必要になったら拡張するためのメモです。

  • 対象: JavaScript/TypeScript のプロジェクトで lint を導入したい人(特に TypeScript/React)
  • この記事で分かること: ESLint の役割、最小構成の入れ方、困ったときの足し方
  • 先に結論: 迷ったらまず Biome を検討し、合わない/使えない場合に ESLint を選びます
  • 結論: まずは推奨ルール + prettier 競合オフで始め、プラグインは必要になってから足します
  • 限界: 既存コードの性質やチーム方針で最適なルールは変わります(まず最小で回します)

先に: Biome と ESLint の比較(結論)

Biome は「フォーマット + lint + import 整理」を 1 つのツールでまとめたいときに強いです。設定が薄く、実行も速いので、これから新規で整えるならまず検討します。

一方で ESLint はエコシステムが巨大で、欲しいルールが見つかりやすく、既存資産(設定/プラグイン/運用)も活かしやすいです。本稿は 「Biome を採用しない(できない)場合」 のために、ESLint を最小構成で始める話に絞ります。

Biome を選びたいケース

  • 新規プロジェクトで「まず整える」を最短でやりたい
  • フォーマッター(prettier)とリンター(ESLint)を分けずに運用したい
  • ルールのカスタマイズは最小でよい(標準ルール中心で OK)

ESLint を選ぶケース(= 本稿の対象)

  • ESLint のプラグイン/ルールセットに依存している(例: a11y, import, unicorn など)
  • 既存プロジェクトで ESLint が前提になっている(移行コストを避けたい)
  • プロジェクト固有の lint(禁止 API など)を細かく入れたい

まとめ

ESLint とは

ESLint は「書き方のルール」を自動で検査するリンターです。 ルールセットに沿って違反を検出し、設定次第では自動修正もできます。プラグイン方式でチェック内容を拡張できるのも特徴です。

ESLint の良いところ

  • 人によってバラバラになってしまいがちな記述方式に一定の統一感を出せます。コードを読む速度が上がります。
  • どちらでもよいときに迷わなくて済み、判断コストが下がります。
  • 変更合戦になりにくくなり、レビューが軽くなります。
  • ルールの追加などをプラグイン方式で行うので、チームでルールの厳しさを決められます。
  • プラグインはルールセットで、個別にオンオフできます(例: react/recommended のような推奨セットもあります)。
  • 利用者が多く、情報も見つけやすいです。

迷ったらこれ(最小構成)

TypeScript/React のプロジェクトなら、まずはこのあとの「最小構成」だけで十分です。困ったらプラグインを足します(最初から盛りすぎない方が安全です)。

最小構成(まず入れるもの)

yarn add -D eslint @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint-config-prettier eslint-plugin-react eslint-plugin-react-hooks

まずは package.jsonscriptslint を追加します。

"scripts": {
  "lint": "eslint --ext .jsx,.js,.tsx,.ts ."
}

自動修正も回したい場合は --fix を付けます(CI では --fix を付けずに失敗させる運用もあります)。

次に .eslintrc.js を作り、まずはこの形から始めます。

module.exports = {
  env: {
    browser: true,
    es2021: true,
  },
  extends: [
    "eslint:recommended",
    "plugin:@typescript-eslint/recommended",
    "plugin:react-hooks/recommended",
    "plugin:react/recommended",
  ],
  globals: {
    Atomics: "readonly",
    SharedArrayBuffer: "readonly",
    React: "writable",
  },
  ignorePatterns: ["build"],
  parser: "@typescript-eslint/parser",
  parserOptions: {
    ecmaFeatures: { jsx: true },
    ecmaVersion: 2021,
    sourceType: "module",
    project: "./tsconfig.json",
  },
  plugins: ["@typescript-eslint", "react"],
  rules: {
    // ここに個別対応したいものを書く
  },
  settings: { react: { version: "detect" } },
};

最後に yarn lint を実行して、まず 1 画面(または 1 ファイル)だけ通る状態を作ります。

yarn lint

--ext は対象とする拡張子、最後の . は検索対象(カレントディレクトリ)です。

細かい設定

無視したいファイル(build/ など)はルートに置いた .eslintignore に書くか、.eslintrc.jsignorePatterns に書きます(node_modules はデフォルトで無視されます)。
その他の設定は .eslintrc.js に記述します(チームごとに差が出やすい部分です)。

おすすめとしては、まず有名なプラグインを最小だけ入れて、xxx/recommended のような推奨ルールセットだけを有効にし、個別ルールは必要になってから触るのが続きます。運用が重くなりがちなので、最初から盛りすぎない方が安全です。

設定で主に触るべきは3箇所です。

  • "plugins":どのプラグインを導入するか(これをしただけではルールはオンになりません)
  • "extends":どのルールセットを導入するか(プラグインを入れなくても、最初から ESLint 内にあるルールセットもあります)
  • "rules":個別対応のルールです

eslint --init を使うと対話的に初期設定できますが、本稿では「まず動く最小構成」をコピーして始める形に寄せます。

プラグイン紹介

最初から全部入れると設定が膨らみ、運用が重くなりがちです。まずは「設定例」の最小構成で始め、困ったときに必要なものを追加する方が続きます。

プラグイン一覧(メモ)
  • "@typescript-eslint/eslint-plugin": TypeScript 用ルールセット
  • "@typescript-eslint/parser": TypeScript を解析するために必要
  • "eslint-config-prettier": prettier と競合するルールをオフにします
  • "eslint-plugin-react": React 用
  • "eslint-plugin-react-hooks": React Hooks 用
  • "eslint-plugin-jsx-a11y": アクセシビリティ関連
  • "eslint-plugin-import": import 周りのルール
  • "eslint-plugin-simple-import-sort": import/export の並び替え
  • "eslint-plugin-eslint-comments": eslint-disable コメントの扱いを整えます
  • "eslint-plugin-prettier": ESLint から prettier を動かしたい場合
  • "eslint-plugin-unicorn": いろいろ盛り(癖があります)
  • "eslint-plugin-sonarjs": SonarJS 由来のルール
  • "eslint-plugin-ava": Ava 用(使っている場合)

たまに無効化したい場合

1 行だけ無効化したい場合、エラーが出た行の 1 行上にコメントで

// eslint-disable-next-line camelcase

JSXElement 内の場合は次のようにします。

{
  /* eslint-disable-next-line jsx-a11y/anchor-has-content */
}

数行分の場合は次のようにします。

/* eslint-disable camelcase */
interface ArticleRecord {
  article_id: number;
  published_at: Date;
  title: string;
}
/* eslint-enable camelcase */

これでファイル全体を囲むと、当然ファイル全体で無効化できます。最後の eslint-enable はなくてもよいですが、あった方がよいかもしれません。

付録(大きめの設定例)

おまけ(ハードコアの実際に使った設定例)

特に "@typescript-eslint/prefer-readonly-parameter-types" は強敵です。ある程度意識して書いていますが、今回はオフにしています。
学びのためなので、eslint-config-prettier も使わずに設定しています。

{
  "env": {
    "browser": true,
    "node": true,
    "es2021": true
  },
  "extends": [
    "eslint:all",
    "plugin:@typescript-eslint/all",
    "plugin:ava/recommended",
    "plugin:eslint-comments/recommended",
    "plugin:import/errors",
    "plugin:import/react",
    "plugin:import/typescript",
    "plugin:import/warnings",
    "plugin:jsx-a11y/strict",
    "plugin:react-hooks/recommended",
    "plugin:react/all",
    "plugin:sonarjs/recommended",
    "plugin:unicorn/recommended"
  ],
  "globals": {
    "Atomics": "readonly",
    "SharedArrayBuffer": "readonly",
    "React": "writable"
  },
  "ignorePatterns": [
    "__memo__",
    "packages/api/dist",
    "packages/app/public/*.js",
    "packages/shared/dist",
    "templates"
  ],
  "parser": "@typescript-eslint/parser",
  "parserOptions": {
    "ecmaFeatures": { "jsx": true },
    "ecmaVersion": 2021,
    "sourceType": "module",
    "project": "./tsconfig.json"
  },
  "plugins": [
    "@typescript-eslint",
    "ava",
    "eslint-comments",
    "import",
    "jsx-a11y",
    "react",
    "simple-import-sort",
    "sonarjs",
    "unicorn"
  ],
  "rules": {
    "jsx-a11y/anchor-is-valid": "off",
    "jsx-a11y/label-has-for": "off",
    "jsx-a11y/no-onchange": "off",

    "unicorn/no-nested-ternary": "off",
    "unicorn/no-null": "off",
    "unicorn/no-useless-undefined": "off",
    "unicorn/prevent-abbreviations": "off",

    "@typescript-eslint/explicit-function-return-type": "off",
    "@typescript-eslint/indent": "off",
    "@typescript-eslint/no-magic-numbers": "off",

    "react/forbid-component-props": "off",
    "react/function-component-definition": "off",
    "react/jsx-child-element-spacing": "off",
    "react/jsx-curly-newline": "off",
    "react/jsx-handler-names": "off",
    "react/jsx-max-props-per-line": "off",
    "react/jsx-newline": "off",
    "react/jsx-no-bind": "off",
    "react/jsx-no-literals": "off",
    "react/jsx-one-expression-per-line": "off",

    "array-bracket-newline": "off",
    "array-element-newline": "off",
    "capitalized-comments": "off",
    "function-call-argument-newline": "off",
    "function-paren-newline": "off",
    "id-length": "off",
    "implicit-arrow-linebreak": "off",
    "line-comment-position": "off",
    "lines-around-comment": "off",
    "max-len": "off",
    "max-lines-per-function": "off",
    "max-statements": "off",
    "multiline-ternary": "off",
    "newline-per-chained-call": "off",
    "no-confusing-arrow": "off",
    "no-continue": "off",
    "no-inline-comments": "off",
    "no-mixed-operators": "off",
    "no-ternary": "off",
    "no-undef-init": "off",
    "no-undefined": "off",
    "no-underscore-dangle": "off",
    "sort-imports": "off",
    "sort-keys": "off",
    "wrap-regex": "off",

    "simple-import-sort/imports": "warn",
    "simple-import-sort/exports": "warn",

    // "unicorn/custom-error-definition": "warn",
    "unicorn/no-keyword-prefix": "warn",
    "unicorn/no-unsafe-regex": "warn",
    "unicorn/no-unused-properties": "warn",
    "unicorn/numeric-separators-style": "warn",
    "unicorn/string-content": "warn",

    "eslint-comments/no-restricted-disable": "warn",
    "eslint-comments/no-unused-disable": "warn",
    // "eslint-comments/no-use": "warn",
    // "eslint-comments/require-description": "warn",

    "ava/no-cb-test": "warn",
    "ava/prefer-power-assert": "warn",
    "ava/test-title-format": "warn",

    "import/no-restricted-paths": "warn",
    "import/no-absolute-path": "warn",
    "import/no-dynamic-require": "warn",
    // "import/no-internal-modules": "warn",
    "import/no-webpack-loader-syntax": "warn",
    "import/no-self-import": "warn",
    "import/no-cycle": "warn",
    "import/no-useless-path-segments": "warn",
    // "import/no-relative-parent-imports": "warn",

    "import/export": "warn",
    "import/no-extraneous-dependencies": "warn",
    "import/no-mutable-exports": "warn",
    "import/no-unused-modules": "warn",

    // "import/unambiguous": "warn",
    "import/no-commonjs": "warn",
    "import/no-amd": "warn",
    "import/no-nodejs-modules": "warn",

    "import/first": "warn",
    "import/exports-last": "warn",
    "import/no-duplicates": "warn",
    "import/no-namespace": "warn",
    "import/extensions": "warn",
    // "import/order": "warn",
    "import/newline-after-import": "warn",
    // "import/prefer-default-export": "warn",
    // "import/max-dependencies": "warn",
    "import/no-unassigned-import": "warn",
    "import/no-named-default": "warn",
    // "import/no-default-export": "warn",
    // "import/no-named-export": "warn",
    "import/no-anonymous-default-export": "warn",
    "import/group-exports": "warn",
    "import/dynamic-import-chunkname": "warn",

    "unicorn/filename-case": [
      "warn",
      { "cases": { "camelCase": true, "pascalCase": true, "kebabCase": true } }
    ],

    "@typescript-eslint/comma-dangle": [
      "warn",
      {
        "arrays": "always-multiline",
        "objects": "always-multiline",
        "imports": "always-multiline",
        "exports": "always-multiline"
      }
    ],
    "@typescript-eslint/member-delimiter-style": [
      "warn",
      {
        "multiline": { "delimiter": "none", "requireLast": false },
        "singleline": { "requireLast": false }
      }
    ],
    "@typescript-eslint/naming-convention": [
      "warn",
      {
        "selector": "default",
        "format": ["strictCamelCase", "StrictPascalCase"]
      },
      {
        "selector": "variable",
        "format": ["strictCamelCase", "StrictPascalCase", "UPPER_CASE"],
        "trailingUnderscore": "allow"
      },
      {
        "selector": "function",
        "format": ["strictCamelCase", "StrictPascalCase"]
      },
      {
        "selector": "parameter",
        "format": ["strictCamelCase"],
        "leadingUnderscore": "allow"
      },
      {
        "selector": "property",
        "format": ["strictCamelCase", "StrictPascalCase"]
      },
      {
        "selector": "parameterProperty",
        "format": ["strictCamelCase"]
      },
      {
        "selector": "method",
        "format": ["strictCamelCase"]
      },
      {
        "selector": "accessor",
        "format": ["strictCamelCase"]
      },
      {
        "selector": "enumMember",
        "format": ["strictCamelCase"]
      },
      {
        "selector": "class",
        "format": ["StrictPascalCase"]
      },
      {
        "selector": "interface",
        "format": ["StrictPascalCase"]
      },
      {
        "selector": "enum",
        "format": ["strictCamelCase"]
      },
      {
        "selector": "typeAlias",
        "format": ["StrictPascalCase"]
      },
      {
        "selector": "typeParameter",
        "format": ["StrictPascalCase"]
      }
    ],
    "@typescript-eslint/no-extra-parens": ["warn", "functions"],
    "@typescript-eslint/no-type-alias": [
      "warn",
      {
        "allowAliases": "always",
        "allowCallbacks": "always",
        "allowConditionalTypes": "always",
        "allowMappedTypes": "always"
      }
    ],
    // "@typescript-eslint/prefer-readonly-parameter-types": [
    //   "warn",
    //   { "ignoreInferredTypes": true }
    // ],
    "@typescript-eslint/prefer-readonly-parameter-types": "off",
    "@typescript-eslint/quotes": "off",
    "@typescript-eslint/object-curly-spacing": ["warn", "always"],
    "@typescript-eslint/semi": ["warn", "never"],
    "@typescript-eslint/space-before-function-paren": [
      "warn",
      { "named": "never" }
    ],

    "react/jsx-filename-extension": [2, { "extensions": [".tsx"] }],
    "react/jsx-indent": [
      "warn",
      2,
      { "checkAttributes": true, "indentLogicalExpressions": true }
    ],
    "react/jsx-indent-props": ["warn", 2],
    "react/jsx-max-depth": ["warn", { "max": 4 }],
    "react/jsx-props-no-spreading": [
      "warn",
      { "custom": "ignore", "explicitSpread": "ignore" }
    ],
    "react/no-multi-comp": ["warn", { "ignoreStateless": true }],

    "dot-location": ["warn", "property"],
    "func-style": ["warn", "declaration", { "allowArrowFunctions": true }],
    "max-classes-per-file": ["warn", 2],
    "no-console": ["warn", { "allow": ["warn", "error"] }],
    "no-void": ["warn", { "allowAsStatement": true }],
    "object-property-newline": [
      "warn",
      { "allowAllPropertiesOnSameLine": true }
    ],
    "one-var": ["warn", "never"],
    "padded-blocks": ["warn", "never"],
    "quote-props": ["warn", "as-needed"],

    "sonarjs/no-duplicate-string": ["warn", 4]
  },
  "settings": { "react": { "version": "detect" } }
}


参考: https://github.com/EvgenyOrekhov/eslint-config-hardcore#readme

関連した話題(補足)

prettier

prettier は「フォーマッターなので ESLint とは分けたい」という方針もあります。その場合は、次のように prettier を別スクリプトで回します。

"scripts":{
  ...
  "format":"prettier  \"**/*\" --write --ignore-unknown",
  ...
}

つまり prettier は yarn add -D prettier で追加し、yarn format のような別コマンドにすると整理しやすいです。利点は、ESLint の適用範囲外(例: HTML/CSS/Markdown)にも prettier を適用できることです。

一方で「ESLint 側から prettier を実行したい」場合は eslint-plugin-prettier を使う選択肢もあります。どちらにしても eslint-config-prettier(競合するルールをオフにします)は入れておくと安心です。

また、prettier の設定は「設定しない」派と「最小だけ設定する」派があります。例えば package.json に次のように書く形です。

  "prettier": {
    "semi": false,
    "singleQuote": true
  },

最初はこの程度の最小設定から始めると迷いにくいです。

husky, lint-staged

huskylint-staged を使うと、コミット時にフォーマットや lint を自動実行できます。lint-stagedgit add 済み(stage 内)のファイルだけを走査できるため、対象を絞って軽く回せます。
ただし、コミットが失敗しやすくなるので、チームの合意がある場合だけ入れるのが安全です。

  "husky": {
    "hooks": {
      "pre-commit": "lint-staged"
    }
  },
  "lint-staged": {
    "**/*": "yarn format",
    "*.{js,ts,tsx}": "yarn lint"
  },

あと

補足として、他に入れていることがあるツールです。

sort-package-jsonpackage.json を整形するためのツールです。

yarn add -D sort-package-json

例えば次のように scripts に組み込みます。

"scripts":{...
  "format": "sort-package-json package.json && prettier \"**/*\" --write --ignore-unknown",...

実務メモ(落とし穴・代替案・検証)

落とし穴

  • 最初からプラグイン/ルールを盛りすぎると運用が破綻しやすいです(まず recommended + 競合オフで回します)
  • 既存コード全体に一気に当てると警告が多くて進みにくいです(1ファイル/1画面から始めます)
  • 自動修正(--fix)を CI で回すと差分が出続けやすいです(ローカルで整形して、CI は検査だけにすると楽です)
  • eslint-disable の乱用で形骸化しやすいです(理由を残し、範囲を最小にします)

代替案と比較軸

  • 整形だけ揃えたい: prettier を単独で回す選択肢もあります
  • 型由来の問題を厳密に見たい: TypeScript の tsc --noEmit を併用します
  • 比較軸: 目的(整形/バグ予防/型)、自動修正の扱い、既存差分の量、CI の厳しさ

判断フロー(最短)

  • Biome を採用できる: まずは Biome(本稿の範囲外)
  • まずは eslint:recommended + eslint-config-prettier
  • TypeScript を使う: @typescript-eslint/recommended
  • React を使う: react/recommended + react-hooks/recommended
  • 困った症状が出てからプラグインを追加します(import 周り、a11y など)

検証(最小)

  • yarn lint がローカル/CI で同じ結果になります
  • --fix を回しても差分が収束します(無限に直り続けません)

参考(一次情報)

更新: 2025-12-29(最小構成を前半に固定、設定例を付録に整理、冒頭に限界と参考(一次情報)を追記、関連話題を折りたたみに整理、落とし穴、代替案、判断フロー、検証観点を追記、Biome との比較を冒頭に追加し、本稿の位置づけを「Biome を使わない場合」に寄せた)

Discussion