0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ESLintとPrettierの役割を正しく分ける

0
Last updated at Posted at 2026-02-10

概要

デフォルトのESLint設定を強化し、TypeScriptとReact/Next.jsのベストプラクティスに沿ったコードを書けるようにする。
また、コードフォーマットツール「Prettier」を導入するで導入したeslint-config-prettierをESLintに組み込み、Prettierとの役割分担を明確にする。

ESLintとPrettierの役割分担

ツール 役割 担当する領域
Prettier フォーマット(見た目) インデント、改行、セミコロン、クォート
ESLint コード品質(ロジック・バグ) 未使用変数、any型、不適切なAPI使用

ESLintにもフォーマット系のルールがあるが、Prettierと競合すると次のような無限ループが起きやすい。

Prettierが整形 → ESLintが「違う」と警告 → 修正 → Prettierが「違う」と戻す → ∞

eslint-config-prettier をESLint設定に追加してフォーマット系ルールを無効化し、
フォーマットはPrettier、コード品質はESLintという分担を確立する。

なぜ必要か

問題: デフォルトのESLint設定だけでは検出できないバグやアンチパターンがある。

例:

// 未使用の変数(メモリの無駄)
const unusedVar = 'never used';

// any型(型安全性の喪失)
const fetchData = async (): Promise<any> => {
  // ...
};

// 不要な文字列の波括弧
<div className={'text-white'}>  {/* 波括弧不要 */}

影響:

  • バグが本番環境で発生
  • 型安全性の恩恵を受けられない
  • コードの可読性が低下

解決: ESLintルールを追加し、開発中にこれらの問題を検出する。

どのように実装するか

1. ESLint設定を強化する

eslint.config.mjs を以下の内容に完全に置き換える

import { defineConfig, globalIgnores } from "eslint/config";
import nextVitals from "eslint-config-next/core-web-vitals";
import nextTs from "eslint-config-next/typescript";
import eslintConfigPrettier from "eslint-config-prettier";

const eslintConfig = defineConfig([
  ...nextVitals,
  ...nextTs,
  {
    rules: {
      // TypeScript関連
      '@typescript-eslint/no-unused-vars': [
        'error',
        {
          argsIgnorePattern: '^_',
          varsIgnorePattern: '^_',
        },
      ],
      '@typescript-eslint/no-explicit-any': 'warn',
      '@typescript-eslint/consistent-type-imports': [
        'error',
        { prefer: 'type-imports' },
      ],

      // React関連
      'react/jsx-curly-brace-presence': [
        'error',
        { props: 'never', children: 'never' },
      ],
      'react/self-closing-comp': 'error',

      // Next.js関連
      '@next/next/no-html-link-for-pages': 'error',

      // 一般的なJavaScript
      'no-console': ['warn', { allow: ['warn', 'error'] }],
      'prefer-const': 'error',
    },
  },
  // Prettierとの競合を回避(必ず最後に配置)
  eslintConfigPrettier,
  globalIgnores([
    ".next/**",
    "out/**",
    "build/**",
    "next-env.d.ts",
    "node_modules/**",
    ".pnpm-store/**",
  ]),
]);

export default eslintConfig;

ポイント:

2. package.jsonにlint:fixを追加

package.jsonscripts を次のように更新する。

{
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "eslint",
    "lint:fix": "eslint --fix",
    "format": "prettier --write \"**/*.{ts,tsx,js,jsx,json,md,css}\"",
    "format:check": "prettier --check \"**/*.{ts,tsx,js,jsx,json,md,css}\""
  }
}

スクリプトの使い分け:

コマンド 動作 用途
pnpm lint エラーの検出のみ CI/CDでのチェック
pnpm lint:fix 自動修正可能なエラーを修正 開発中のクイック修正
pnpm format Prettierでフォーマット 見た目の整形
pnpm format:check フォーマット違反の検出のみ CI/CDでのチェック

追加したルールの詳細

1. @typescript-eslint/no-unused-vars

何を検出するか: 宣言されているが使われていない変数

// ❌ エラー
const unusedVariable = 'never used';
const result = calculate(); // resultを使っていない

// ✅ OK: _プレフィックスで意図的に無視
const _internalVar = 'used internally';
const [_, setCount] = useState(0); // 最初の値は使わない

なぜ必要か:

  • 不要なコードを削除してバンドルサイズを減らす
  • コードの可読性向上
  • 変数名のタイポを検出

2. @typescript-eslint/no-explicit-any

何を検出するか: any 型の使用

// ⚠️ 警告
const fetchData = async (): Promise<any> => {
  return await fetch('/api/data').then((res) => res.json());
};

// ✅ OK: 適切な型定義
interface ApiResponse {
  data: Member[];
  success: boolean;
}

const fetchData = async (): Promise<ApiResponse> => {
  return await fetch('/api/data').then((res) => res.json());
};

なぜ必要か:

  • TypeScriptの型安全性を最大限活用
  • IDEの補完が効く
  • 実行時エラーを減らす

注意: error ではなく warn にしているのは、外部ライブラリとの統合等で any が避けられない場合があるため。

3. @typescript-eslint/consistent-type-imports

何を検出するか: 型のインポートに type キーワードを使っていない

// ❌ エラー: 型なのにtype修飾子がない
import { Member } from '@/types';

// ✅ OK: type importを使用
import type { Member } from '@/types';

// ✅ OK: 値と型を混在させる場合
import { fetchMembers, type Member } from '@/lib/members';

なぜ必要か:

  • バンドルサイズの削減(type import はビルド時に完全に除去される)
  • 値のインポートと型のインポートの意図が明確になる

4. react/jsx-curly-brace-presence

何を検出するか: 不要な波括弧 {} の使用

// ❌ エラー
<div className={'text-white'}>
  {'Hello'}
</div>

// ✅ OK
<div className="text-white">
  Hello
</div>

// ✅ OK: 変数や式の場合は波括弧が必要
<div className={isActive ? 'active' : 'inactive'}>
  {userName}
</div>

なぜ必要か:

  • 余分なコードを減らす
  • 可読性向上

5. react/self-closing-comp

何を検出するか: 子要素のないコンポーネントで閉じタグを使っている

// ❌ エラー
<Image src="/logo.png" alt="Logo"></Image>
<div></div>

// ✅ OK
<Image src="/logo.png" alt="Logo" />
<div /> {/* 空のdivは通常避けるべきだが、構文としては正しい */}

// ✅ OK: 子要素がある場合は閉じタグが必要
<div>
  <p>Content</p>
</div>

なぜ必要か:

  • コードが簡潔になる
  • Reactの慣習に従う

6. @next/next/no-html-link-for-pages

何を検出するか: 内部リンクで <a> タグを使っている

// ❌ エラー
<a href="/members">メンバー一覧</a>

// ✅ OK
import Link from 'next/link';

<Link href="/members">メンバー一覧</Link>

なぜ必要か:

  • Next.jsの<Link>を使うことでクライアントサイドルーティングが有効になる
  • ページ遷移が高速になる
  • プリフェッチが自動で行われる

7. no-console

何を検出するか: console.log の使用

// ⚠️ 警告
console.log('Debug info');

// ✅ OK
console.warn('Warning message');
console.error('Error message');

// ✅ OK: 開発中のデバッグ用に一時的に使う場合
// eslint-disable-next-line no-console
console.log('Temporary debug');

なぜ必要か:

  • 本番環境に不要なログを残さない
  • パフォーマンスへの影響を避ける
  • 適切なログレベル(warn, error)を使う習慣をつける

8. prefer-const

何を検出するか: 再代入されない変数を let で宣言している

// ❌ エラー
let name = 'John';
let age = 30;
console.log(name, age); // 再代入されていない

// ✅ OK
const name = 'John';
const age = 30;

// ✅ OK: 再代入される場合はletが必要
let count = 0;
count++; // 再代入

なぜ必要か:

  • 変数の不変性を明示
  • バグを減らす(意図しない再代入を防ぐ)
  • コードの意図が明確になる

動作確認

1. ESLintを実行

pnpm lint

エラーがなければ:

✔ No ESLint warnings or errors

2. 意図的にエラーを起こしてテスト

/app/page.tsx に以下を追加する。

export default function Home() {
  const unusedVar = 'test'; // 未使用変数
  let name = 'John'; // 再代入されないlet

  return (
    <div className={'container'}> {/* 不要な波括弧 */}
      <div></div> {/* 自己閉じタグにできる */}
      <a href="/about">About</a> {/* Next.js Linkを使うべき */}
    </div>
  );
}

ESLintを実行:

pnpm lint

以下のようなエラーが表示される。

app/page.tsx
  2:9   error  'unusedVar' is assigned a value but never used  @typescript-eslint/no-unused-vars
  3:7   error  'name' is never reassigned. Use 'const' instead  prefer-const
  6:11  error  JSX attribute value has unnecessary curly braces  react/jsx-curly-brace-presence
  7:7   error  Empty components are self-closing                react/self-closing-comp
  8:7   error  Do not use an `<a>` element to navigate to `/about`. Use `<Link />` from `next/link` instead  @next/next/no-html-link-for-pages

3. 自動修正を試す

pnpm lint:fix

自動修正可能なエラー(prefer-constjsx-curly-brace-presenceself-closing-comp)が修正される。
未使用変数の削除など、コードの意味が変わる修正は手動で対応する。

4. エラーを修正

import Link from 'next/link';

export default function Home() {
  const name = 'John';

  return (
    <div className="container">
      <Link href="/about">About</Link>
    </div>
  );
}

再度ESLintを実行:

pnpm lint

エラーが消える。

✔ No ESLint warnings or errors

CI/CDでの自動チェック

ローカルでの手動実行だけでは、チーム全体のコード品質を担保できない。
CI/CDに組み込むことで品質チェックを自動化する。

GitHub Actionsの例

.github/workflows/lint.yml を作成する。

name: Lint & Format Check

on: [push, pull_request]

jobs:
  lint:
    runs-on: ubuntu-latest
    defaults:
      run:
        working-directory: ./web  # それぞれプロジェクトルートからの相対パスに変更
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'pnpm'
      - run: pnpm install --frozen-lockfile
      - run: pnpm lint
      - run: pnpm format:check

ポイント:

  • PRを出すたびにESLintとPrettierのチェックが自動で走る
  • pnpm lintでコード品質チェック、pnpm format:checkでフォーマットチェック
  • チェックが通らないとマージできないように設定すれば品質を維持できる

特定のルールを無効にしたい

一時的に無効化する場合:

// 1行だけ無効化
// eslint-disable-next-line no-console
console.log('Debug');

// ファイル全体で無効化(非推奨)
/* eslint-disable @typescript-eslint/no-explicit-any */
const data: any = fetchData();
/* eslint-enable @typescript-eslint/no-explicit-any */

恒久的に無効化する場合は eslint.config.mjs のルールから削除する。

PrettierとESLintが競合する

eslint-config-prettier を設定に組み込んでいれば競合は発生しない。
もし競合が起きた場合は、次を確認する。

  1. eslintConfigPrettier がルール定義より後ろに配置されているか
  2. ESLintのキャッシュを削除: pnpm lint --cache --cache-location .eslintcache
  3. node_modules を再インストール

発展: 型情報を使った高度なルール

@typescript-eslint にはTypeScriptの型情報を活用した強力なルールがある。
通常のルールでは検出できない実行時バグを見つけられる。

代表的なルール

no-floating-promises -- awaitを忘れたPromiseを検出

// ❌ awaitを忘れている(エラーが握りつぶされる)
async function saveUser(user: User) {
  fetch('/api/users', { method: 'POST', body: JSON.stringify(user) });
}

// ✅ OK
async function saveUser(user: User) {
  await fetch('/api/users', { method: 'POST', body: JSON.stringify(user) });
}

no-misused-promises -- Promiseの不適切な使用を検出

// ❌ onClickにasync関数を渡している(エラーが捕捉されない)
<button onClick={async () => { await saveUser(user); }}>Save</button>

// ✅ OK: エラーハンドリングを追加
<button onClick={() => { saveUser(user).catch(console.error); }}>Save</button>

設定方法

// eslint.config.mjs に追加
{
  languageOptions: {
    parserOptions: {
      projectService: true,
      tsconfigRootDir: import.meta.dirname,
    },
  },
  rules: {
    '@typescript-eslint/no-floating-promises': 'error',
    '@typescript-eslint/no-misused-promises': 'error',
    '@typescript-eslint/await-thenable': 'error',
  },
}

注意: 型情報を使うルールはリンティング速度が低下するため、プロジェクトの規模と相談して導入する。
小から中規模なら問題なく使える。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?