asで逃げるのをやめる──Claude Codeに判別共用体と型ガードを書かせるCLAUDE.md設計

はじめに

Claude CodeにTypeScriptを書かせると、型エラーをas SomeTypeで黙らせたコードが出てくることがある。コンパイルは通るが型チェックをバイパスしているため、実行時エラーの温床になる。CLAUDE.mdに「何をしてはいけないか」を明記すれば、型ガード関数と判別共用体を使った安全なコードが出てくる。本記事はanyの次の問題──as型アサーション・判別共用体の設計ミス・ユーティリティ型の未活用──に絞って扱う。


Claude Codeが生成するTypeScriptコードで気になるパターン

`as`型アサーションの乱用

型エラーを解消する最短路としてas SomeTypeが出てくる。as constはリテラル型推論のための演算子であり問題ない。問題なのはas Useras stringのように型チェックを強制突破するアサーションだ。


// ❌ asで型チェックをバイパスする
const user = response.data as User;
const id = someValue as string;

外部データ(APIレスポンス・URLパラメータ等)には型ガード関数を書く。


// ✅ 型ガード関数で安全に絞り込む
function isUser(v: unknown): v is User {
  return typeof v === 'object' && v !== null &&
    typeof (v as Record<string, unknown>).id === 'string' &&
    typeof (v as Record<string, unknown>).name === 'string';
}

const data: unknown = response.data;
if (isUser(data)) { console.log(data.name); } // User型として安全に扱える

判別共用体(Discriminated Union)の設計ミス

共通の判別子プロパティがない、または判別子がstring型になるとexhaustive checkが機能しなくなる。


// ❌ 判別子がない・型が広すぎる
type ApiResponse = { status: string; data?: User; error?: Error };
type Shape = { radius: number } | { width: number; height: number };

// ✅ 文字列リテラルの判別子を持つ判別共用体
type Shape =
  | { kind: 'circle'; radius: number }
  | { kind: 'rectangle'; width: number; height: number };

function describeShape(shape: Shape): string {
  switch (shape.kind) {
    case 'circle': return `circle r=${shape.radius}`;
    case 'rectangle': return `rect ${shape.width}x${shape.height}`;
    default:
      const _exhaustive: never = shape; // 新variantが追加されたらここでエラー
      return _exhaustive;
  }
}

ユーティリティ型の活用不足

PartialOmitを使わず、同じプロパティを手書きで並べた型を定義する。元の型が変わっても追従しないため乖離が生じる。


// ❌ 手書きで冗長な型定義(User型と二重管理になる)
interface UserUpdateInput { name?: string; email?: string; }

// ✅ ユーティリティ型で元の型から派生させる
type UserUpdateInput = Partial<Omit<User, 'id' | 'createdAt'>>;
type CreateUserInput = Omit<User, 'id' | 'createdAt'>;
type UserFormState = Partial<CreateUserInput> & { isSubmitting: boolean };

TypeScript型設計向けCLAUDE.md完全テンプレート


# TypeScript型設計ルール

## 禁止パターン
- `as SomeType`型アサーション禁止(`as const`・`as unknown`は許可)
- `any`型禁止 / `// @ts-ignore` 禁止
- 外部データ(APIレスポンス・localStorage等)は `function isXxx(v: unknown): v is Xxx` 型ガード関数で検証(src/types/guards.ts に集約)

## 判別共用体(Discriminated Union)設計ルール
- union型には文字列リテラル型の判別子(`kind`・`type`・`status`)を設ける
- switchのdefaultで `const _exhaustive: never = variable` のexhaustive checkを行う

## ユーティリティ型・高度な型パターン
- 既存型からの派生はPartial/Omit/Pick/Recordを使う(手書き列挙は禁止)
- `satisfies`演算子で型制約を検証する(型アノテーションの代替)
- Template Literal Types: イベント名・APIルートのパターン定義
- `infer` / Conditional Types: Promiseのアンラップ・ReturnType抽出

PostToolUseフックで型検証を自走させる

.ts.tsxファイルの編集後にtsc --noEmitを自動実行する設定だ。Claude Codeが型エラーを受け取り、その場で修正するループが回る。


{
  "hooks": {
    "PostToolUse": [{
      "matcher": "Edit|Write|MultiEdit",
      "hooks": [{
        "type": "command",
        "command": "file=$(jq -r '.tool_input.file_path // \"\"'); echo \"$file\" | grep -qE '\\.tsx?$' && npx tsc --noEmit --skipLibCheck 2>&1 | head -30",
        "timeout": 30000
      }]
    }]
  }
}

型アサーション違反はESLintで静的検出できる。@typescript-eslint/consistent-type-assertions: ["error", { "assertionStyle": "never" }]を追加し、as constallowConstAssertions: trueで別途許可する。


EM視点──型安全チームへの移行戦略

CLAUDE.mdに型ルールを書くと、チーム全員のClaude Codeセッションで同じ制約が働く。個人の意識に依存せず型品質のベースラインが上がる。

即日で「as SomeType禁止」をCLAUDE.mdに追記する。これで新規コードに型アサーションが入らなくなる。1週間以内にPostToolUseフックのtsc自動実行を有効化し、.husky/pre-commitにも同じゲートを追加する。既存コードはgrep -r " as " src/ --include="*.ts"で件数を計測し型ガード化をClaude Codeに依頼する。チーム展開ではCLAUDE.mdをmonorepoルートに配置する。


まとめ

as SomeTypeが出てくる根本原因は、型アサーション禁止のルールがCLAUDE.mdで伝わっていないことだ。「as SomeType禁止・外部データは型ガード関数で検証・union型には文字列リテラル型判別子を設ける」の3行を追記してほしい。PostToolUseフックでtscを自動実行すれば、Claude Codeが型エラーをその場で修正するようになる。

コメント

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