テクノロジー
型で防げるバグ、防げないバグ
型を厳しくしても、バグがゼロにならない理由
TypeScript を strict にして、any を禁止して、型カバレッジを上げる。それでも本番でバグは出る。よくあるのは「型を厳しくしたのに減らなかった」という感想だが、これは型の役割を見誤っているサインだ。型が保証するのは「コンパイル時点で分かる形の整合性」だけで、実行時に何が起きるかまでは保証しない。
まずは「型で消えるバグ」と「型では消えないバグ」を分けて考える。
型で防げるバグ・防げないバグの分類
型で防げるバグ
- プロパティ名のタイプミス(
user.nmae) - 関数への引数の渡し忘れ・型の取り違え
- null / undefined の考慮漏れ(
strictNullChecks有効時) - リファクタ時の呼び出し側の更新漏れ
これらはコンパイラが機械的に検出できる。型を厳しくする投資はここで確実に効く。
型では防げないバグ
- 境界を越えて入ってくる値 — API レスポンス、フォーム入力、localStorage、外部 SDK のコールバック。型定義は「こうあってほしい」という宣言であり、実行時に本当にその形である保証はない
- ロジックの誤り — 型は合っているが、計算式や条件分岐そのものが間違っている(例: 割引率の適用順序、丸め誤差、境界値の
<と<=の取り違え) - 状態遷移・タイミングの誤り — 非同期処理の完了順序、競合状態、二重送信。型システムは「いつ」を扱わない
役割分担: 型・テスト・実行時検証
この3つは競合ではなく補完関係にある。
- 型: コンパイル時に検出できる形の誤りを潰す。人間のミスと機械的な取り違えに強い
- テスト: ロジックの正しさを担保する。型が通っていても計算が間違っていることを検出できるのはテストだけ
- 実行時検証: 型が及ばない「境界」を守る。API レスポンスやフォーム入力は、届いた瞬間に検証してから型を信じる
境界での実行時検証の入れどころ
典型的な失敗は「API クライアントの戻り値に型注釈を付けて満足する」パターンだ。
// アンチパターン: 型注釈だけで「検証した」気になる
const res = await fetch("/api/v1/bug_reports/1");
const data: BugReport = await res.json(); // 実行時は何も保証されていない
これは型がついているだけで、実際にサーバーが古いバージョンを返した場合や、フィールドが null の場合を一切検出できない。zod のようなスキーマバリデーションライブラリで境界を明示的に守る。
const BugReportSchema = z.object({
id: z.number(),
status: z.enum(["open", "in_progress", "closed"]),
screenshotPath: z.string(),
});
const res = await fetch("/api/v1/bug_reports/1");
const parsed = BugReportSchema.safeParse(await res.json());
if (!parsed.success) {
// ここで初めて「実行時に型が破られた」ことを検出できる
reportSchemaViolation(parsed.error);
throw new Error("unexpected bug_reports response shape");
}
境界検証を入れるべき箇所は限られている。すべてのオブジェクトに zod を付けるのではなく、「自分のコードの外から来る値」だけに絞るのが費用対効果が良い。
チェックリスト: この値は型で信じていいか
- この値はどこから来たか。自分のコード内の計算結果か、外部(API・フォーム・ストレージ・SDK)から来た値か
- 外部由来なら、届いた瞬間に検証しているか。型注釈だけで済ませていないか
- 型は合っているが、計算やビジネスロジックが正しいことをテストで担保しているか
- 非同期処理の順序が変わっても壊れない設計になっているか(型では検出できない)
Bugoon での実践
境界での検証漏れや状態遷移の誤りは、型チェックやユニットテストをすり抜けて本番で初めて表面化しやすい。Bugoon はブラウザ上でのバグ報告時に、スクリーンショットと操作ステップ、Console / Network のエラーログを自動で添付するため、「型は合っていたのに実行時に壊れた」ケースでも、実際にどんな値が届いていたかの手がかりが報告に残る。報告はそのまま GitHub Issue として起票でき、MCP サーバー経由で Claude Code や Cursor に再現手順ごと渡せるので、境界のどこで想定外の値が入り込んだかを追いやすい。
← 記事一覧に戻るBugoon Blog