メインコンテンツまでスキップ

第3章 なぜバグる?チェックが散ると事故る😱🌀

第3章 なぜバグる?チェックが散ると事故る😱🌀

この章は「不変条件を守る前に、なんで守れなくなるのか?」を体感する回だよ〜🙂✨ いったん “あるある地獄” を見てから、次章以降の「入口で一回だけ保証する🚪🛡️」がスッと入るようにするね🫶🌸


1) この章でできるようになること🎯✨

  • ✅ 「チェックが散る」と、なぜ漏れるのか説明できる🙂
  • ✅ “if のコピペ増殖”が、バグと保守コストを増やす流れがわかる😵‍💫
  • ✅ 「入口で一回だけ保証する」発想の気持ちよさをつかむ🚪🛡️
  • ✅ TypeScriptの型だけでは守れない領域(実行時)も理解する👀💡 ※ TypeScriptの型注釈はコンパイル後に消える=実行中の安全は別で守る必要があるよ📌 (typescriptlang.org)

2) まず結論:チェックが散ると “漏れ” が必ず出る😱

チェックが散るコードって、だいたいこうなるの👇

  • 入口が複数(UI / API / DB / 外部API)🌍➡️
  • それぞれの場所で「一応チェック」する😅
  • でも 微妙に違うチェック が混ざる(trimしてる/してない、上限が違う、条件が古い…)🌀
  • 仕様変更で「全部探して直す」になって、どこか必ず直し忘れる💥

つまり…

“どこで保証されてるか分からない” = どこでも壊れる 😇💣


3) あるある例:会員登録(Emailと年齢)✉️🎓

ここでは題材として「会員登録」を使うよ🙂 守りたい不変条件はこんな感じ👇

  • Email:空じゃない、だいたいメールっぽい(例:@ がある)✉️
  • 年齢:0〜120の範囲内👶➡️👵
  • 表示名:空白だけはダメ🙅‍♀️

4) 悪い例:チェックが散って “漏れ” が出るコード😵‍💫🧨

チェックが散って “漏れ” が出るコード

「とりあえず動く」を積み重ねると、こうなりやすい…!

// どこからでも呼ばれる便利関数(のつもり)
function createUser(email: string, age: number, displayName: string) {
// チェック①(ここではtrimしてない)
if (!email.includes("@")) throw new Error("email invalid");
if (age < 0) throw new Error("age invalid");

return { email, age, displayName };
}

function handleUiSubmit(form: { email: string; age: string; displayName: string }) {
// チェック②(ここではtrimしてる)
const email = form.email.trim();
if (email.length === 0) return { ok: false, message: "email required" };

// ageを数値化(ここではNaNチェックしてない)
const age = Number(form.age);

return { ok: true, user: createUser(email, age, form.displayName) };
}

function importFromCsv(row: { email: string; age: string; displayName: string }) {
// チェック③(ここではageの上限だけ見てる)
const age = Number(row.age);
if (age > 120) throw new Error("age too old");

// emailのチェックを忘れた😇
return createUser(row.email, age, row.displayName);
}

何が起きる?😱

  • CSVから取り込むと、メールが空でも通る(チェック漏れ)💥
  • UI経由だとtrimされるけど、別経由だと " a@b.com" がそのまま残る(正規化の不一致)🌀
  • Number(form.age)NaN でも通って、後で計算が爆発する💣

そして一番つらいのがこれ👇

「直すとき」が地獄😵‍💫🧹

  • 仕様変更:「年齢は 13歳以上」になった! → どこに age チェックがあるか探す → 直し忘れが出る → “特定ルートだけバグる” が発生😇

5) さらに重要:TypeScriptの型だけでは、入口は守れない🛡️❓

ここ、超だいじ🙂✨ TypeScriptは 開発中(コンパイル時) に助けてくれるけど、

  • 実行時には型注釈が消える
  • “型があるから安全” にはならない

って公式ドキュメントでもハッキリ書かれてるよ📌 「型注釈は実行時の動きを変えない」ってやつ! (typescriptlang.org)

だから外部入力(フォーム、API、CSV、DB、外部API)は 実行時にチェック が必要になるのね🙂


6) 良い考え方:入口で一回だけ保証する🚪🛡️✨

合言葉はこれ👇

✅ 「境界で検証して変換」→「中は信じる」🙂🏰

  • 境界(入口)で unknown / string みたいな “信用できない値” を受け取る🕵️‍♀️
  • そこでまとめて検証する🧪
  • OKなら「検証済みの形」に変換してから中へ渡す💎
  • ドメイン内部では “再チェックしない” を目指す(チェック散らばりを止める)🧼✨

この発想が、次章の「境界(Boundary)を見つけよう🚧📍」につながるよ〜🙂✨


7) 改善例:入口に “検問所” を作る🚓🚪✨

ポイントは 「ドメインの関数は、検証済みだけ受け取る」 だよ🙂

// 入口が作る「検証済みのデータ」
type SignupInput = {
email: string; // ここではまだプリミティブだけど「検証済み」の意味
age: number;
displayName: string;
};

// 入口の検問所(ここにチェックを集約する)
function parseSignup(raw: unknown): { ok: true; value: SignupInput } | { ok: false; errors: string[] } {
const errors: string[] = [];

if (typeof raw !== "object" || raw === null) {
return { ok: false, errors: ["request must be an object"] };
}

const obj = raw as Record<string, unknown>;

const email = typeof obj.email === "string" ? obj.email.trim() : "";
if (email.length === 0) errors.push("email required");
if (email.length > 0 && !email.includes("@")) errors.push("email invalid");

const age = typeof obj.age === "number" ? obj.age : Number(obj.age);
if (!Number.isFinite(age)) errors.push("age must be a number");
if (Number.isFinite(age) && (age < 0 || age > 120)) errors.push("age out of range");

const displayName = typeof obj.displayName === "string" ? obj.displayName.trim() : "";
if (displayName.length === 0) errors.push("displayName required");

if (errors.length > 0) return { ok: false, errors };

return { ok: true, value: { email, age, displayName } };
}

```ts
// ドメイン側:もう「検証済み」しか来ない前提に寄せる
function createUser(input: SignupInput) {
// ここでは基本、検証しない(散らばり防止)
return { id: crypto.randomUUID(), ...input };
}

何が嬉しい?😍✨

  • チェックが1箇所に集まる → 直す場所が明確🧭
  • UIでもCSVでもAPIでも、入口が同じ検問所を通るようにできる🚪🚪🚪
  • ドメイン側のコードがスッキリして “意図” が見える🙂💎

8) 今どきの流れ(最新の周辺事情)📰✨

この「入口で実行時検証」文化はますます強くなってるよ🙂 最近はスキーマ検証ライブラリも進化していて、たとえば Zod は v4 が安定版として案内されてるよ📦✨ (Zod) さらに、Zod/Valibot/ArkType などで共通インターフェースを作ろうという “Standard Schema” の流れもある(ライブラリ横断で扱いやすくする動き)🧩🤝 (Zenn)

(この教材でも後半で「スキーマ→型が付く」気持ちよさをちゃんとやるよ〜😌✨)

※ TypeScript自体も高速化のためにネイティブ実装(TypeScript 7のプレビュー等)の話が進んでるけど、これは主に速度・開発体験の改善の方向だよ🚀 (Microsoft Developer)


9) ミニ課題(15〜30分)📝✨

課題A:散ってるチェック探しゲーム🔍🌀

手元のコード(または想像の題材)で👇を探してみてね🙂

  • 同じような if が複数箇所にある
  • trimしてる場所としてない場所がある
  • Number変換してるのに NaN チェックがない
  • 例外/戻り値がバラバラ(UIだけ優しいメッセージ、他は即throw…)

課題B:入口関数を1個だけ作る🚪🛡️

  • どこか1つ(例:フォーム送信)だけでいいから 「parse〇〇」みたいな関数を作って、チェックを集約してみよう🙂✨

10) AI活用テンプレ(この章ver)🤖💡✨

そのままコピペで使えるよ〜😆💕

  • 「このコードで入力チェックが散ってる箇所を列挙して。漏れそうな観点も追加して」🔍
  • 「この不変条件(例:年齢0〜120)を破る入力パターンを20個出して」🧠
  • 「バリデーションを入口の関数に集約したい。分離案と関数名案を出して」🚪✨
  • 「“途中で検証” が残ってたら指摘して、削除していい根拠も説明して」🧹🙂

まとめ✨

  • チェックが散ると、漏れる・直し忘れる・ルート限定バグが出る 😱
  • TypeScriptの型は実行時には消えるから、入口は実行時チェックが必要 🧪 (typescriptlang.org)
  • だから「入口で一回だけ保証する🚪🛡️」が効く!

次の第4章では、その「入口=境界」を具体的に見つけていくよ〜🚧📍✨