TypeScript any撲滅リファクタリング:strictモード移行から CI強制化まで
なぜ any を放置してはいけないのか
any はTypeScriptの型システムを「無効化」するエスケープハッチです。一時的な回避策として書いたはずが、コードレビューをすり抜けてコードベース全体に伝染し、最終的にはランタイムエラーが頻発する「型なしJavaScript」に逆戻りします。
実際に筆者が参加した中規模プロジェクトでは、any が300箇所以上に散在し、APIレスポンスの型ズレに起因するバグが週1回以上発生していました。本記事では、既存コードを壊さずに any を段階的に除去し、CIで再発防止する手順を解説します。
Step 1:現状把握 — any の棚卸し
まずは敵の数を数えます。
# anyの出現箇所を一覧表示
grep -rn ': any' src/ --include='*.ts' --include='*.tsx' | wc -l
# ESLintで詳細レポート
npx eslint 'src/**/*.{ts,tsx}' \
--rule '{"@typescript-eslint/no-explicit-any": "warn"}' \
--format json > any-report.json
@typescript-eslint/no-explicit-any をまず warn にするのがポイントです。いきなり error にすると既存CIが全落ちします。
優先度マトリクス
| 箇所 | リスク | 優先度 |
|---|---|---|
| APIレスポンスの型 | 高(実行時エラー直結) | 🔴 最高 |
| 関数の引数・戻り値 | 高(型推論が伝染停止) | 🔴 最高 |
| イベントハンドラ | 中 | 🟡 中 |
| テストコード内 | 低 | 🟢 低 |
Step 2:tsconfig を段階的に strict 化
"strict": true を一気に有効にすると大量のエラーが噴出します。フラグを1つずつ有効化する戦略を取ります。
// tsconfig.json — 段階的strict化の例
{
"compilerOptions": {
// Phase 1: まずここから(比較的安全)
"strictNullChecks": true,
"noImplicitReturns": true,
// Phase 2: 1〜2週間後
"noImplicitAny": true, // ← anyの暗黙使用を禁止
"strictFunctionTypes": true,
// Phase 3: 安定したら
"strict": true // 上記を含む全フラグ有効化
}
}
noImplicitAny を有効にすると、明示的に書いていない暗黙の any(引数の型注釈漏れなど)がエラーになります。これだけでも相当数の問題が炙り出されます。
Step 3:any の置き換えパターン集
パターン① APIレスポンス → zod でバリデーション
// Before
const fetchUser = async (id: string): Promise<any> => {
const res = await fetch(`/api/users/${id}`);
return res.json();
};
// After: zodでスキーマ定義 + パース
import { z } from 'zod';
const UserSchema = z.object({
id: z.string(),
name: z.string(),
email: z.string().email(),
});
type User = z.infer<typeof UserSchema>;
const fetchUser = async (id: string): Promise<User> => {
const res = await fetch(`/api/users/${id}`);
const data = await res.json();
return UserSchema.parse(data); // 型不一致はここで例外
};
パターン② 「何でも受け取る」関数 → ジェネリクスに置換
// Before
function identity(arg: any): any {
return arg;
}
// After
function identity<T>(arg: T): T {
return arg;
}
パターン③ イベントハンドラ
// Before
const handleChange = (e: any) => {
console.log(e.target.value);
};
// After
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
console.log(e.target.value);
};
パターン④ 外部ライブラリの型がない場合 → unknown + 型ガード
// Before
function processData(data: any) { /* ... */ }
// After: anyではなくunknownを使い、型ガードで絞り込む
function isRecord(v: unknown): v is Record<string, unknown> {
return typeof v === 'object' && v !== null;
}
function processData(data: unknown) {
if (!isRecord(data)) throw new Error('Invalid data');
// ここから data は Record<string, unknown> として扱える
}
anyvsunknownの使い分け:どちらも「型不明」ですが、unknownは操作前に型ガードが必須なので安全。新しく書くコードではunknownを使いましょう。
Step 4:ESLint ルールで再発防止
.eslintrc.json または eslint.config.js に以下を追加します。
// .eslintrc.json(抜粋)
{
"rules": {
"@typescript-eslint/no-explicit-any": "error", // anyを禁止
"@typescript-eslint/no-unsafe-assignment": "error", // anyの代入も禁止
"@typescript-eslint/no-unsafe-call": "error",
"@typescript-eslint/no-unsafe-member-access": "error",
"@typescript-eslint/no-unsafe-return": "error"
}
}
既存コードに多数残っている場合は、eslint-disable コメントを自動挿入してから徐々に解除していく手法も有効です。
# 既存違反箇所に一括でdisableコメントを挿入するツール
npx @typescript-eslint/eslint-plugin-bulk-suppress \
--rule @typescript-eslint/no-explicit-any \
'src/**/*.{ts,tsx}'
Step 5:CI で型チェックを強制化
ローカルでいくら直しても、PRで新しい any が入ってくれば元の木阿弥です。CIのゲートに組み込むことが必須です。
GitHub Actions の例
# .github/workflows/typecheck.yml
name: Type Check
on:
pull_request:
paths:
- 'src/**/*.ts'
- 'src/**/*.tsx'
- 'tsconfig*.json'
jobs:
typecheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- name: TypeScript type check
run: npx tsc --noEmit
- name: ESLint (any check)
run: npx eslint 'src/**/*.{ts,tsx}' --max-warnings 0
--max-warnings 0 がポイントで、警告が1件でもあればCIを落とす強制力を持たせます。
段階的にwarning → errorへ昇格させる運用
Week 1: no-explicit-any を warn → CIはpass、棚卸しだけ
Week 2: 優先度高の箇所を修正
Week 3: no-explicit-any を error に変更 → 以降CIがゲートに
まとめ:any撲滅ロードマップ
| フェーズ | 作業内容 | 目安期間 |
|---|---|---|
| 棚卸し | grep / ESLint warnで全量把握 | 1日 |
| tsconfig | strictNullChecks → noImplicitAny 段階投入 | 1〜2週 |
| コード修正 | zod・ジェネリクス・型ガードで置換 | 2〜4週 |
| ESLint強化 | no-explicit-any: error + 関連ルール追加 | 1日 |
| CI強制化 | GitHub ActionsでPRゲート | 1日 |
any の除去は一度に全部やろうとすると挫折します。棚卸し → 優先度高から修正 → CIで再発防止のサイクルを小さく回すのが成功の鍵です。型安全なコードベースは、長期的にはバグ修正コストを大幅に下げてくれます。腰を据えて取り組む価値は十分にあります。