💡 Tips

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> として扱える
}

any vs unknown の使い分け:どちらも「型不明」ですが、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}'
📚 おすすめ書籍

プログラミングTypeScript ―スケールするJavaScriptアプリケーション開発

型設計の考え方が体系的に学べる良書

Amazonで見る →

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日
tsconfigstrictNullChecksnoImplicitAny 段階投入1〜2週
コード修正zod・ジェネリクス・型ガードで置換2〜4週
ESLint強化no-explicit-any: error + 関連ルール追加1日
CI強制化GitHub ActionsでPRゲート1日

any の除去は一度に全部やろうとすると挫折します。棚卸し → 優先度高から修正 → CIで再発防止のサイクルを小さく回すのが成功の鍵です。型安全なコードベースは、長期的にはバグ修正コストを大幅に下げてくれます。腰を据えて取り組む価値は十分にあります。

📚 おすすめ書籍

TypeScript実践プログラミング

実務コードへの型付けノウハウが豊富な一冊

Amazonで見る →