概要
デフォルトの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;
ポイント:
-
コードフォーマットツール「Prettier」を導入するでインストールした
eslint-config-prettierを最後に追加 -
@typescript-eslint/consistent-type-importsを追加 -
eslintConfigPrettierは必ず末尾に置く
2. package.jsonにlint:fixを追加
package.json の scripts を次のように更新する。
{
"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';
なぜ必要か:
- バンドルサイズの削減(
typeimport はビルド時に完全に除去される) - 値のインポートと型のインポートの意図が明確になる
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-const、jsx-curly-brace-presence、self-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 を設定に組み込んでいれば競合は発生しない。
もし競合が起きた場合は、次を確認する。
-
eslintConfigPrettierがルール定義より後ろに配置されているか - ESLintのキャッシュを削除:
pnpm lint --cache --cache-location .eslintcache -
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',
},
}
注意: 型情報を使うルールはリンティング速度が低下するため、プロジェクトの規模と相談して導入する。
小から中規模なら問題なく使える。