ESLint 9 + Prettier 3完全設定ガイド〜Flat Config・typescript-eslint・React・Next.js〜

「ESLint 9でFlat Configに変わって設定がわからない」「PrettierとESLintのルール衝突をどう避ける?」「React・TypeScript・Next.jsでベストな組み合わせは?」——本記事はESLint 9.x(Flat Config)+ Prettier 3.xを軸に、ゼロからのインストールからtypescript-eslint・React・Next.js・Husky・GitHub Actionsまで、コピペで動く実コード40本以上で解説する完全設定ガイドです。eslint-config-prettierでの責務分離、prettier-plugin-tailwindcssの順序統一、lint-stagedによるpre-commit、CI/CDでの差分lintまで、2026年時点での実戦標準を一気通貫で組み立てます。

※ 個別フレームワーク(Next.js / Remix / Vite等)の詳細は姉妹記事 frameworks一覧 を参照してください。本記事は「すべてのプロジェクトに必要な共通土台」としてのlinter/formatter設計に集中します。

  1. ESLint 9 + Prettier 3の全体像 — 2026年の設定事情
    1. Flat Configの基本思想
    2. ESLintとPrettierの責務分離 — 鉄則
    3. 必要バージョン早見表(2026年5月時点)
  2. ESLint 9 のインストールと最小構成 — まず1ファイルで動かす
    1. ESLintのインストール
    2. 最小のeslint.config.js(CommonJSなし、ESM固定)
    3. package.json側のtype設定
    4. 初回実行とエラーの読み方
    5. commonjsで書きたい場合の例(レガシー対応)
  3. files / ignores / globalIgnores — パスマッチの設計
    1. globalIgnores相当 — プロジェクト全体で除外
    2. filesでファイルタイプ別にルールを分ける
    3. filesとignoresは配列単位で適用される
  4. typescript-eslint v8 — TypeScript対応のセットアップ
    1. typescript-eslintのインストール
    2. tseslint.config()でTS基本セットを組む
    3. projectService(v8の新型解決)を有効化
    4. strict / stylisticの違い
    5. 型を必要とするルール群(type-checked)
  5. React + React Hooks + JSX a11y — フロント標準セット
    1. Reactプラグイン群のインストール
    2. React向けのeslint.config.js
    3. react-hooksルールの効果
    4. jsx-a11yで防げる典型例
  6. eslint-plugin-import / unicorn / vitest — 品質強化プラグイン
    1. importプラグイン
    2. import順序を強制する
    3. unicornプラグイン — モダンJSのベストプラクティス
    4. vitestプラグイン — テストコード専用
  7. Prettier 3 のインストールと基本設定
    1. Prettierのインストール
    2. .prettierrc — プロジェクト共通の整形ルール
    3. .prettierignore
    4. Prettierのコマンド実行
    5. package.jsonにスクリプト追加
  8. eslint-config-prettier — 衝突を完全に止める
    1. インストールと設定
    2. eslint-plugin-prettierは使わない(推奨2026年)
    3. 衝突しているか確認するヘルパー
  9. prettier-plugin-tailwindcss / プラグイン群
    1. Tailwindクラスの自動並び替え
    2. 並び替え前後の例
    3. importを並び替えるプラグイン
    4. プラグインの読込順注意
  10. Next.js プロジェクトでの設定
    1. Next.js向けのインストール
    2. eslint.config.js(Next.js 15)
    3. next lintは2026年に廃止予定
    4. App Router固有のルール
  11. レガシー .eslintrc.* からの移行ガイド
    1. 公式の移行コマンド
    2. extendsの典型置換パターン
    3. env置換
    4. parserOptions置換
    5. 段階的移行のテクニック
  12. VSCode連携 — formatOnSave / codeActionsOnSave
    1. .vscode/settings.json — チーム共有設定
    2. .vscode/extensions.json — 推奨拡張
    3. 「explicit」と「always」の違い
  13. Husky + lint-staged — pre-commit hook
    1. Huskyのセットアップ
    2. .husky/pre-commit
    3. package.jsonへのlint-staged設定
    4. ステージ済みファイルだけに当てる仕組み
    5. 巨大プロジェクトでの最適化
    6. commit時のtsc型チェック(任意)
  14. GitHub Actions — CIでの lint チェック
    1. .github/workflows/lint.yml
    2. 差分ファイルだけlintする(高速化)
    3. キャッシュの活用
    4. PRコメントに結果を投稿
  15. tsconfig との連携 — 型情報を共有する
    1. tsconfig.eslint.json — リント専用
    2. eslint.config.js側で参照
    3. projectServiceで自動探索(推奨)
    4. monorepo対応
  16. 運用Tips — キャッシュ・ルール設計・トラブルシュート
    1. ESLint キャッシュを有効化
    2. ルールはerror / warn / offの3階層で運用
    3. 「特定行だけ無効化」コメント
    4. –debug でルール解決をトレース
    5. –print-config で実効設定を確認
    6. typescript-eslintが遅いとき
  17. まとめ — 2026年のESLint + Prettier鉄板構成
    1. 最終形:全部入りeslint.config.js
    2. 最終形:.prettierrc
    3. 最終形:package.json scripts
    4. 関連記事

ESLint 9 + Prettier 3の全体像 — 2026年の設定事情

2024年4月にリリースされたESLint v9でFlat Config(eslint.config.js)がデフォルトとなり、長らく標準だった.eslintrc.* + extends方式は終了しました。同時にPrettierもv3に上がり、ESM対応・末尾カンマのデフォルト変更・プラグインAPIの整理が行われました。「過去の.eslintrc.jsonから書き写したらどこにも当たらない」状態を避けるため、まずは思想を押さえます。

Flat Configの基本思想

  • JS/MJS/TSファイル1枚で完結: extendsは廃止。配列を ...spread で合成
  • 明示的なfiles / ignores: パスマッチが配列単位で明確
  • plugin/parserはオブジェクト渡し: 文字列指定の魔術が消えた
  • 環境(env)廃止 → languageOptions.globals: globalsパッケージで明示

ESLintとPrettierの責務分離 — 鉄則

「ESLintで整形もする」のはアンチパターンです。役割を以下のように完全分離します。

役割 担当 対象
コード品質(バグ検出) ESLint 未使用変数・型の誤用・hooksルール・a11y等
整形(見た目) Prettier インデント・改行・カンマ・引用符
衝突回避 eslint-config-prettier ESLintの整形系ルールを無効化

必要バージョン早見表(2026年5月時点)

ツール 推奨バージョン 備考
Node.js 20.x LTS or 22.x 22.x推奨
ESLint 9.15.x 以上 Flat Configデフォルト
typescript-eslint 8.15.x 以上 v8でFlat完全対応
Prettier 3.3.x 以上 末尾カンマall既定
TypeScript 5.5 以上 moduleResolution: “bundler”

ESLint 9 のインストールと最小構成 — まず1ファイルで動かす

いきなり巨大な設定を書く前に、最小構成で「動く」状態を作ります。これがすべての出発点です。

ESLintのインストール

# pnpm利用前提(npm/yarnでもOK)
pnpm add -D eslint@^9 @eslint/js globals

# 動作確認
pnpm exec eslint --version
# v9.15.0

最小のeslint.config.js(CommonJSなし、ESM固定)

// eslint.config.js
import js from "@eslint/js";
import globals from "globals";

/** @type {import('eslint').Linter.Config[]} */
export default [
  js.configs.recommended,
  {
    files: ["**/*.{js,mjs,cjs}"],
    languageOptions: {
      ecmaVersion: 2024,
      sourceType: "module",
      globals: {
        ...globals.node,
        ...globals.browser,
      },
    },
    rules: {
      "no-unused-vars": "warn",
      "no-console": ["warn", { allow: ["warn", "error"] }],
    },
  },
];

package.json側のtype設定

{
  "name": "my-app",
  "type": "module",
  "scripts": {
    "lint": "eslint .",
    "lint:fix": "eslint . --fix"
  }
}

初回実行とエラーの読み方

pnpm lint
# /src/index.js
#   3:7  warning  'unused' is assigned a value but never used
#
# /src/api.js
#   5:1  warning  Unexpected console statement

# 自動修正可能なものだけ直す
pnpm lint:fix

commonjsで書きたい場合の例(レガシー対応)

// eslint.config.cjs(package.json に "type": "module" を入れない場合)
const js = require("@eslint/js");
const globals = require("globals");

module.exports = [
  js.configs.recommended,
  {
    languageOptions: {
      globals: { ...globals.node },
    },
  },
];

files / ignores / globalIgnores — パスマッチの設計

Flat Configの肝は「どのファイルにどのルールを当てるか」を配列で明示することです。.eslintignoreは廃止され、ignoresキーで宣言します。

globalIgnores相当 — プロジェクト全体で除外

// eslint.config.js
import js from "@eslint/js";

export default [
  // プロジェクト全体で常に除外したいパス
  {
    ignores: [
      "node_modules/**",
      "dist/**",
      "build/**",
      ".next/**",
      "coverage/**",
      "**/*.min.js",
      "public/**",
      "**/generated/**",
    ],
  },

  js.configs.recommended,
];

filesでファイルタイプ別にルールを分ける

export default [
  // TypeScriptソース
  {
    files: ["src/**/*.{ts,tsx}"],
    rules: {
      "no-console": "error",
    },
  },

  // テストファイルだけはconsole許可
  {
    files: ["**/*.{test,spec}.{ts,tsx}", "tests/**/*"],
    rules: {
      "no-console": "off",
      "no-magic-numbers": "off",
    },
  },

  // configファイルはNode.jsグローバル
  {
    files: ["*.config.{js,ts,mjs}", "scripts/**/*"],
    languageOptions: {
      globals: { ...require("globals").node },
    },
  },
];

filesとignoresは配列単位で適用される

// 「TypeScriptファイルにだけrecommendedルールを当てる」例
import tseslint from "typescript-eslint";

export default [
  ...tseslint.configs.recommended.map((config) => ({
    ...config,
    files: ["**/*.{ts,tsx}"],
  })),
];

typescript-eslint v8 — TypeScript対応のセットアップ

2024年に出たtypescript-eslint v8でFlat Configに完全対応し、tseslint.config()ヘルパーで型安全に書けるようになりました。

typescript-eslintのインストール

pnpm add -D typescript typescript-eslint

# 単独パッケージで入れる場合(細かい制御が必要なとき)
pnpm add -D @typescript-eslint/parser @typescript-eslint/eslint-plugin

tseslint.config()でTS基本セットを組む

// eslint.config.js
import js from "@eslint/js";
import tseslint from "typescript-eslint";
import globals from "globals";

export default tseslint.config(
  { ignores: ["dist/**", ".next/**", "node_modules/**"] },

  js.configs.recommended,
  ...tseslint.configs.strict,
  ...tseslint.configs.stylistic,

  {
    files: ["**/*.{ts,tsx}"],
    languageOptions: {
      ecmaVersion: 2024,
      sourceType: "module",
      globals: { ...globals.browser, ...globals.node },
      parserOptions: {
        projectService: true,
        tsconfigRootDir: import.meta.dirname,
      },
    },
    rules: {
      "@typescript-eslint/consistent-type-imports": "error",
      "@typescript-eslint/no-unused-vars": [
        "error",
        { argsIgnorePattern: "^_", varsIgnorePattern: "^_" },
      ],
      "@typescript-eslint/no-explicit-any": "warn",
    },
  },
);

projectService(v8の新型解決)を有効化

// 旧:project: true (重い・遅い)
// 新:projectService: true(必要なtsconfigを動的探索・高速)
{
  languageOptions: {
    parserOptions: {
      projectService: true,
      tsconfigRootDir: import.meta.dirname,
    },
  },
}

strict / stylisticの違い

// strict = バグになりうるパターンを厳しめに検出
// stylistic = 一貫性のあるTypeScript的スタイル
// 推奨:両方入れる(整形系はPrettier担当だが「型の書き方」は別物)
export default tseslint.config(
  ...tseslint.configs.strict,
  ...tseslint.configs.stylistic,
);

型を必要とするルール群(type-checked)

// より厳密な型情報ベースのルールを使いたいときは
export default tseslint.config(
  ...tseslint.configs.strictTypeChecked,
  ...tseslint.configs.stylisticTypeChecked,
  {
    files: ["**/*.{ts,tsx}"],
    languageOptions: {
      parserOptions: { projectService: true, tsconfigRootDir: import.meta.dirname },
    },
  },
);

React + React Hooks + JSX a11y — フロント標準セット

ReactプロジェクトではESLintで「Hooksルール違反」「key忘れ」「a11y欠落」を防ぐのが定石です。3プラグインで構成します。

Reactプラグイン群のインストール

pnpm add -D 
  eslint-plugin-react 
  eslint-plugin-react-hooks 
  eslint-plugin-jsx-a11y 
  globals

React向けのeslint.config.js

import js from "@eslint/js";
import tseslint from "typescript-eslint";
import react from "eslint-plugin-react";
import reactHooks from "eslint-plugin-react-hooks";
import jsxA11y from "eslint-plugin-jsx-a11y";
import globals from "globals";

export default tseslint.config(
  { ignores: ["dist/**", "build/**"] },
  js.configs.recommended,
  ...tseslint.configs.recommended,

  {
    files: ["**/*.{jsx,tsx}"],
    plugins: {
      react,
      "react-hooks": reactHooks,
      "jsx-a11y": jsxA11y,
    },
    languageOptions: {
      parserOptions: {
        ecmaFeatures: { jsx: true },
      },
      globals: { ...globals.browser },
    },
    settings: {
      react: { version: "detect" },
    },
    rules: {
      ...react.configs.recommended.rules,
      ...react.configs["jsx-runtime"].rules,
      ...reactHooks.configs.recommended.rules,
      ...jsxA11y.configs.recommended.rules,

      // React 17+ では import React 不要
      "react/react-in-jsx-scope": "off",
      "react/prop-types": "off",
    },
  },
);

react-hooksルールの効果

// ❌ rules-of-hooksに違反 — 条件分岐内でhooks
function Bad({ flag }: { flag: boolean }) {
  if (flag) {
    const [x, setX] = useState(0); // eslint: React Hook called conditionally
  }
  return null;
}

// ❌ exhaustive-deps — depsに変数漏れ
function Bad2({ id }: { id: string }) {
  useEffect(() => {
    fetch(`/api/${id}`);
  }, []); // eslint: React Hook useEffect has a missing dependency 'id'
}

jsx-a11yで防げる典型例

// ❌ alt属性が欠落
<img src="/logo.png" />

// ❌ クリックハンドラだけでキーボード操作不可
<div onClick={handleClick}>Click</div>

// ⭕ button要素を使う or role/tabIndex/onKeyDownを追加
<button type="button" onClick={handleClick}>Click</button>

eslint-plugin-import / unicorn / vitest — 品質強化プラグイン

標準ルールだけでは検出しきれない「importの順序」「危ない記法」「テスト記述漏れ」をプラグインで補強します。

importプラグイン

pnpm add -D eslint-plugin-import eslint-import-resolver-typescript

import順序を強制する

import importPlugin from "eslint-plugin-import";

export default [
  {
    plugins: { import: importPlugin },
    settings: {
      "import/resolver": {
        typescript: { project: "./tsconfig.json" },
        node: true,
      },
    },
    rules: {
      "import/order": [
        "error",
        {
          groups: [
            "builtin",
            "external",
            "internal",
            ["parent", "sibling", "index"],
            "type",
          ],
          "newlines-between": "always",
          alphabetize: { order: "asc", caseInsensitive: true },
        },
      ],
      "import/no-duplicates": "error",
      "import/no-cycle": ["error", { maxDepth: 3 }],
    },
  },
];

unicornプラグイン — モダンJSのベストプラクティス

pnpm add -D eslint-plugin-unicorn
import unicorn from "eslint-plugin-unicorn";

export default [
  unicorn.configs["flat/recommended"],
  {
    rules: {
      // ファイル名のkebab-case強制が厳しすぎる場合
      "unicorn/filename-case": [
        "error",
        { cases: { kebabCase: true, pascalCase: true } },
      ],
      // 過度に制限的なルールはoffにする
      "unicorn/prevent-abbreviations": "off",
      "unicorn/no-null": "off",
    },
  },
];

vitestプラグイン — テストコード専用

pnpm add -D eslint-plugin-vitest @vitest/eslint-plugin
import vitest from "@vitest/eslint-plugin";

export default [
  {
    files: ["**/*.{test,spec}.{ts,tsx}"],
    plugins: { vitest },
    rules: {
      ...vitest.configs.recommended.rules,
      "vitest/expect-expect": "error",
      "vitest/no-focused-tests": "error",
      "vitest/no-disabled-tests": "warn",
    },
  },
];

Prettier 3 のインストールと基本設定

整形はPrettierに完全委譲します。ESLintでindentquotesを設定するのは禁止です。

Prettierのインストール

pnpm add -D prettier@^3

# バージョン確認
pnpm exec prettier --version
# 3.3.3

.prettierrc — プロジェクト共通の整形ルール

{
  "printWidth": 100,
  "tabWidth": 2,
  "useTabs": false,
  "semi": false,
  "singleQuote": true,
  "quoteProps": "as-needed",
  "jsxSingleQuote": false,
  "trailingComma": "all",
  "bracketSpacing": true,
  "bracketSameLine": false,
  "arrowParens": "always",
  "endOfLine": "lf"
}

.prettierignore

# 自動生成・依存・ビルド成果物
node_modules
dist
build
.next
coverage
public
*.min.js
*.min.css
pnpm-lock.yaml
package-lock.json
yarn.lock

Prettierのコマンド実行

# すべて整形チェック
pnpm exec prettier --check .

# 整形して書き戻す
pnpm exec prettier --write .

# 差分があるファイルだけ
pnpm exec prettier --write "src/**/*.{ts,tsx,js,jsx,json,md,css}"

package.jsonにスクリプト追加

{
  "scripts": {
    "format": "prettier --write .",
    "format:check": "prettier --check .",
    "lint": "eslint .",
    "lint:fix": "eslint . --fix",
    "check": "pnpm format:check && pnpm lint"
  }
}

eslint-config-prettier — 衝突を完全に止める

ESLintの整形系ルール(quotes, indent等)とPrettierが衝突すると、save時に修正が往復します。eslint-config-prettierでESLint側の整形系を全部offにします。

インストールと設定

pnpm add -D eslint-config-prettier
// eslint.config.js — 配列の最後に必ず置く
import js from "@eslint/js";
import tseslint from "typescript-eslint";
import prettierConfig from "eslint-config-prettier";

export default tseslint.config(
  js.configs.recommended,
  ...tseslint.configs.recommended,

  // ↓ 必ず最後。整形系ルールを全部offにする
  prettierConfig,
);

eslint-plugin-prettierは使わない(推奨2026年)

// ❌ NG:eslint-plugin-prettier で Prettier を ESLint 経由で走らせる
// → 遅い・エラー出力が読みにくい・editor連携が二重起動になる
// 公式も非推奨。Prettierは独立CLI / IDE拡張で走らせる

// ⭕ 正解:eslint-config-prettierでルール衝突だけ止める

衝突しているか確認するヘルパー

# config-prettierにはcheck CLIがある
pnpm exec eslint-config-prettier src/index.ts
# No rules that are unnecessary or conflict with Prettier were found.

prettier-plugin-tailwindcss / プラグイン群

Prettier 3のプラグインAPIはシンプルになり、.prettierrcpluginsキーで列挙するだけです。

Tailwindクラスの自動並び替え

pnpm add -D prettier-plugin-tailwindcss
{
  "plugins": ["prettier-plugin-tailwindcss"],
  "tailwindFunctions": ["clsx", "cn", "cva", "tw"],
  "tailwindConfig": "./tailwind.config.ts"
}

並び替え前後の例

// before
<div className="text-white p-4 flex bg-blue-500 items-center justify-center" />

// after(プラグインが自動でcanonical順に並び替え)
<div className="flex items-center justify-center bg-blue-500 p-4 text-white" />

importを並び替えるプラグイン

pnpm add -D @ianvs/prettier-plugin-sort-imports
{
  "plugins": [
    "@ianvs/prettier-plugin-sort-imports",
    "prettier-plugin-tailwindcss"
  ],
  "importOrder": [
    "<BUILTIN_MODULES>",
    "^react$",
    "^next(/.*)?$",
    "<THIRD_PARTY_MODULES>",
    "",
    "^@/(.*)$",
    "^[./]"
  ],
  "importOrderTypeScriptVersion": "5.5.0"
}

プラグインの読込順注意

{
  "plugins": [
    "@ianvs/prettier-plugin-sort-imports",
    "prettier-plugin-tailwindcss"
  ]
}
// tailwindプラグインは必ず配列の最後。
// 公式ドキュメントの指定:他のformatプラグインと協調するためtailwindは末尾固定。

Next.js プロジェクトでの設定

Next.js 15はeslint-config-nextがFlat Config対応(next/core-web-vitals)。create-next-appでもFlat Configが既定になりました。

Next.js向けのインストール

pnpm add -D 
  eslint@^9 
  @eslint/js 
  typescript-eslint 
  eslint-config-next 
  eslint-config-prettier 
  globals

eslint.config.js(Next.js 15)

import js from "@eslint/js";
import tseslint from "typescript-eslint";
import nextPlugin from "@next/eslint-plugin-next";
import reactHooks from "eslint-plugin-react-hooks";
import prettierConfig from "eslint-config-prettier";

export default tseslint.config(
  { ignores: [".next/**", "node_modules/**", "out/**"] },
  js.configs.recommended,
  ...tseslint.configs.recommended,

  {
    files: ["**/*.{ts,tsx}"],
    plugins: {
      "@next/next": nextPlugin,
      "react-hooks": reactHooks,
    },
    rules: {
      ...nextPlugin.configs.recommended.rules,
      ...nextPlugin.configs["core-web-vitals"].rules,
      ...reactHooks.configs.recommended.rules,
    },
  },

  prettierConfig,
);

next lintは2026年に廃止予定

# Next.js 15.3+ では next lint がdeprecatedに
# package.jsonを差し替え
{
  "scripts": {
    "lint": "eslint .",
    "lint:fix": "eslint . --fix"
  }
}

App Router固有のルール

{
  files: ["app/**/*.{ts,tsx}"],
  rules: {
    // server componentでuseStateを使ってないかチェック
    "@next/next/no-html-link-for-pages": "off",
    // imageは<Image>を使う
    "@next/next/no-img-element": "error",
  },
}

レガシー .eslintrc.* からの移行ガイド

「v8系の.eslintrc.jsonがある」「extendsで継承していた」プロジェクトを段階的にFlat Configへ移します。

公式の移行コマンド

# @eslint/migrate-config が変換を試みる
pnpm dlx @eslint/migrate-config .eslintrc.json
# → eslint.config.mjs が生成される

extendsの典型置換パターン

// before(eslintrc)
{
  "extends": [
    "eslint:recommended",
    "plugin:@typescript-eslint/recommended",
    "plugin:react/recommended",
    "prettier"
  ]
}

// after(flat config)
import js from "@eslint/js";
import tseslint from "typescript-eslint";
import react from "eslint-plugin-react";
import prettierConfig from "eslint-config-prettier";

export default [
  js.configs.recommended,
  ...tseslint.configs.recommended,
  react.configs.flat.recommended,
  prettierConfig,
];

env置換

// before
{ "env": { "browser": true, "node": true, "es2024": true } }

// after
import globals from "globals";
{
  languageOptions: {
    ecmaVersion: 2024,
    globals: { ...globals.browser, ...globals.node },
  },
}

parserOptions置換

// before
{
  "parser": "@typescript-eslint/parser",
  "parserOptions": {
    "project": "./tsconfig.json",
    "ecmaVersion": 2022,
    "sourceType": "module"
  }
}

// after
{
  languageOptions: {
    parser: tseslint.parser,
    parserOptions: {
      projectService: true,
      tsconfigRootDir: import.meta.dirname,
      ecmaVersion: 2024,
      sourceType: "module",
    },
  },
}

段階的移行のテクニック

# ESLINT_USE_FLAT_CONFIG=falseで一時的にv8互換動作
ESLINT_USE_FLAT_CONFIG=false pnpm eslint .

# 逆に新旧両立できる @eslint/eslintrc を使う(過渡期のみ)
pnpm add -D @eslint/eslintrc
// 旧.eslintrcの設定を一時的に取り込むブリッジ
import { FlatCompat } from "@eslint/eslintrc";

const compat = new FlatCompat({ baseDirectory: import.meta.dirname });

export default [
  ...compat.extends("airbnb-typescript"),
  // ↑ Flat Config化されていないプリセットを一時的に持ち込める
];

VSCode連携 — formatOnSave / codeActionsOnSave

エディタ統合は「保存時に自動で整形・自動修正」の体験が肝です。

.vscode/settings.json — チーム共有設定

{
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  "editor.formatOnSave": true,
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": "explicit",
    "source.organizeImports": "never"
  },
  "eslint.useFlatConfig": true,
  "eslint.experimental.useFlatConfig": true,
  "eslint.workingDirectories": [{ "mode": "auto" }],
  "eslint.validate": [
    "javascript",
    "javascriptreact",
    "typescript",
    "typescriptreact"
  ],
  "prettier.requireConfig": true,
  "[typescript]": { "editor.defaultFormatter": "esbenp.prettier-vscode" },
  "[typescriptreact]": { "editor.defaultFormatter": "esbenp.prettier-vscode" },
  "[json]": { "editor.defaultFormatter": "esbenp.prettier-vscode" }
}

.vscode/extensions.json — 推奨拡張

{
  "recommendations": [
    "dbaeumer.vscode-eslint",
    "esbenp.prettier-vscode",
    "bradlc.vscode-tailwindcss",
    "yoavbls.pretty-ts-errors"
  ]
}

「explicit」と「always」の違い

// "explicit" → Ctrl+S(明示的な保存)でだけ実行
// "always"   → autoSaveでも毎回実行(うるさい)
// 推奨は "explicit"
"source.fixAll.eslint": "explicit"

Husky + lint-staged — pre-commit hook

「ローカルでlintしてからpushしてほしい」を強制するのがpre-commit hookです。Husky 9 + lint-staged 15が定番。

Huskyのセットアップ

pnpm add -D husky lint-staged

# Husky 9はワンライナーで初期化
pnpm exec husky init
# → .husky/pre-commit が生成される

.husky/pre-commit

#!/usr/bin/env sh
pnpm exec lint-staged

package.jsonへのlint-staged設定

{
  "lint-staged": {
    "*.{js,jsx,ts,tsx}": [
      "eslint --fix",
      "prettier --write"
    ],
    "*.{json,md,css,html,yml,yaml}": [
      "prettier --write"
    ]
  }
}

ステージ済みファイルだけに当てる仕組み

# commit時に変更のあるファイルだけ実行される
git add src/foo.ts
git commit -m "feat: add foo"
# ✔ Preparing lint-staged...
# ✔ Running tasks for staged files...
#   ✔ eslint --fix
#   ✔ prettier --write

巨大プロジェクトでの最適化

{
  "lint-staged": {
    "*.{ts,tsx}": [
      "eslint --fix --cache --cache-location .eslintcache",
      "prettier --write --cache"
    ]
  }
}
// --cacheで2回目以降が高速化

commit時のtsc型チェック(任意)

{
  "lint-staged": {
    "*.{ts,tsx}": [
      "eslint --fix",
      "prettier --write",
      "bash -c 'tsc --noEmit'"
    ]
  }
}

GitHub Actions — CIでの lint チェック

pre-commitだけでは「すり抜け」が起こりえます。CI側でPR毎にチェックすることで品質を担保します。

.github/workflows/lint.yml

name: Lint

on:
  pull_request:
    branches: [main]
  push:
    branches: [main]

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: pnpm/action-setup@v4
        with:
          version: 9

      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: pnpm

      - name: Install
        run: pnpm install --frozen-lockfile

      - name: Lint
        run: pnpm lint

      - name: Format check
        run: pnpm format:check

      - name: Type check
        run: pnpm exec tsc --noEmit

差分ファイルだけlintする(高速化)

      - name: Get changed files
        id: changed
        uses: tj-actions/changed-files@v45
        with:
          files: |
            **/*.ts
            **/*.tsx

      - name: Lint changed files
        if: steps.changed.outputs.any_changed == 'true'
        run: pnpm exec eslint ${{ steps.changed.outputs.all_changed_files }}

キャッシュの活用

      - name: Cache ESLint
        uses: actions/cache@v4
        with:
          path: .eslintcache
          key: eslint-${{ runner.os }}-${{ hashFiles('**/eslint.config.js') }}

PRコメントに結果を投稿

      - name: ESLint with annotations
        uses: reviewdog/action-eslint@v1
        with:
          reporter: github-pr-review
          eslint_flags: "src/"

tsconfig との連携 — 型情報を共有する

typescript-eslintはtsconfig.jsonを読みます。ESLint側との認識を合わせないと「リント通るのにビルドで落ちる」現象が起きます。

tsconfig.eslint.json — リント専用

{
  "extends": "./tsconfig.json",
  "include": [
    "src/**/*",
    "tests/**/*",
    "*.config.ts",
    "*.config.js",
    "eslint.config.js"
  ],
  "exclude": ["node_modules", "dist"]
}

eslint.config.js側で参照

{
  languageOptions: {
    parserOptions: {
      project: "./tsconfig.eslint.json",
      tsconfigRootDir: import.meta.dirname,
    },
  },
}

projectServiceで自動探索(推奨)

// 手動でtsconfigを指定する必要なし
{
  languageOptions: {
    parserOptions: {
      projectService: true,
      tsconfigRootDir: import.meta.dirname,
    },
  },
}
// プロジェクトサービスがファイルに最も近いtsconfigを自動選択

monorepo対応

// pnpm workspace の各パッケージにtsconfigがある場合
{
  languageOptions: {
    parserOptions: {
      projectService: {
        allowDefaultProject: ["*.js", "*.mjs"],
        defaultProject: "./tsconfig.json",
      },
      tsconfigRootDir: import.meta.dirname,
    },
  },
}

運用Tips — キャッシュ・ルール設計・トラブルシュート

動かしたあとは「速く・壊れず・チームで揃う」運用が大事です。

ESLint キャッシュを有効化

{
  "scripts": {
    "lint": "eslint . --cache --cache-location node_modules/.cache/eslint/"
  }
}

ルールはerror / warn / offの3階層で運用

// "error" → CIで失敗。コミット禁止
// "warn"  → 警告のみ。徐々に直したい新ルール
// "off"   → 完全無効化

{
  rules: {
    "@typescript-eslint/no-explicit-any": "warn",   // 既存コードがあるので段階導入
    "@typescript-eslint/no-unused-vars": "error",  // 必ず守る
    "react/prop-types": "off",                      // TypeScript環境では不要
  },
}

「特定行だけ無効化」コメント

// 1行だけ無効化
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const data: any = await res.json()

// 範囲で無効化
/* eslint-disable no-console */
console.log("debug 1")
console.log("debug 2")
/* eslint-enable no-console */

–debug でルール解決をトレース

pnpm exec eslint src/index.ts --debug 2>&1 | head -50
# どのconfigがどの順で当たったか全て確認できる

–print-config で実効設定を確認

pnpm exec eslint --print-config src/index.ts > effective-config.json
# あるファイルに対する最終的なルール集合をJSON出力

typescript-eslintが遅いとき

# 1. projectService に切り替える(v8の新解決)
# 2. tsconfig.eslint.jsonで includes を絞る
# 3. --cache を必ず付ける
# 4. パッケージを最新へ
pnpm up -L typescript-eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin

まとめ — 2026年のESLint + Prettier鉄板構成

本記事で組み上げた構成を最後に1ファイルに集約して掲載します。このままeslint.config.jsとして置けば、TypeScript + React + Next.js + Prettierがすべて噛み合います。

最終形:全部入りeslint.config.js

import js from "@eslint/js";
import tseslint from "typescript-eslint";
import react from "eslint-plugin-react";
import reactHooks from "eslint-plugin-react-hooks";
import jsxA11y from "eslint-plugin-jsx-a11y";
import importPlugin from "eslint-plugin-import";
import unicorn from "eslint-plugin-unicorn";
import nextPlugin from "@next/eslint-plugin-next";
import prettierConfig from "eslint-config-prettier";
import globals from "globals";

export default tseslint.config(
  {
    ignores: [
      "node_modules/**",
      "dist/**",
      "build/**",
      ".next/**",
      "out/**",
      "coverage/**",
      "**/*.min.js",
    ],
  },

  js.configs.recommended,
  ...tseslint.configs.strict,
  ...tseslint.configs.stylistic,
  unicorn.configs["flat/recommended"],

  {
    files: ["**/*.{ts,tsx,js,jsx}"],
    plugins: {
      react,
      "react-hooks": reactHooks,
      "jsx-a11y": jsxA11y,
      import: importPlugin,
      "@next/next": nextPlugin,
    },
    languageOptions: {
      ecmaVersion: 2024,
      sourceType: "module",
      parserOptions: {
        projectService: true,
        tsconfigRootDir: import.meta.dirname,
        ecmaFeatures: { jsx: true },
      },
      globals: { ...globals.browser, ...globals.node },
    },
    settings: {
      react: { version: "detect" },
      "import/resolver": {
        typescript: { project: "./tsconfig.json" },
        node: true,
      },
    },
    rules: {
      ...react.configs.recommended.rules,
      ...react.configs["jsx-runtime"].rules,
      ...reactHooks.configs.recommended.rules,
      ...jsxA11y.configs.recommended.rules,
      ...nextPlugin.configs["core-web-vitals"].rules,

      "react/react-in-jsx-scope": "off",
      "react/prop-types": "off",
      "@typescript-eslint/consistent-type-imports": "error",
      "@typescript-eslint/no-unused-vars": [
        "error",
        { argsIgnorePattern: "^_" },
      ],
      "import/order": [
        "error",
        {
          groups: [
            "builtin",
            "external",
            "internal",
            ["parent", "sibling", "index"],
            "type",
          ],
          "newlines-between": "always",
          alphabetize: { order: "asc", caseInsensitive: true },
        },
      ],
      "unicorn/prevent-abbreviations": "off",
      "unicorn/no-null": "off",
    },
  },

  {
    files: ["**/*.{test,spec}.{ts,tsx}"],
    rules: {
      "no-console": "off",
      "@typescript-eslint/no-explicit-any": "off",
    },
  },

  // 最後に必ずprettier。整形系のESLintルールを全部offに
  prettierConfig,
);

最終形:.prettierrc

{
  "printWidth": 100,
  "tabWidth": 2,
  "semi": false,
  "singleQuote": true,
  "trailingComma": "all",
  "arrowParens": "always",
  "endOfLine": "lf",
  "plugins": [
    "@ianvs/prettier-plugin-sort-imports",
    "prettier-plugin-tailwindcss"
  ],
  "importOrder": [
    "<BUILTIN_MODULES>",
    "^react$",
    "^next(/.*)?$",
    "<THIRD_PARTY_MODULES>",
    "",
    "^@/(.*)$",
    "^[./]"
  ]
}

最終形:package.json scripts

{
  "scripts": {
    "lint": "eslint . --cache",
    "lint:fix": "eslint . --cache --fix",
    "format": "prettier --write .",
    "format:check": "prettier --check .",
    "typecheck": "tsc --noEmit",
    "check": "pnpm format:check && pnpm lint && pnpm typecheck",
    "prepare": "husky"
  },
  "lint-staged": {
    "*.{ts,tsx,js,jsx}": ["eslint --fix --cache", "prettier --write"],
    "*.{json,md,css,yml}": ["prettier --write"]
  }
}

関連記事

個別フレームワークの設定例はframeworksカテゴリ一覧から、TypeScript周辺はTypeScriptカテゴリから辿れます。

「設定しても続けるのが難しい」「実プロジェクトで他人のコードと折り合いを付けたい」と感じたら、現場経験ベースで質問できる学習環境が役に立ちます。テックアカデミーのフロントエンドコース侍エンジニアのマンツーマン指導DMM WEBCAMP、即戦力人材としてはレバテックの求人検索などを比較すると、自分の段階に合った選び方ができます。

コメント

タイトルとURLをコピーしました