第09章:TypeScriptの型でテストを減らす🛡️📘

この章はね、「テストをサボる」じゃなくて「そもそも壊れにくくして、テストの量を減らす」回だよ〜!🧁 TypeScriptの型って、上手に使うと “不正な入力・不正な状態” をコンパイル時点で入れないようにできるの。すると、毎回テストで守ってた “つまづきポイント” がごっそり減るよ!😆✨
ちなみに最近のTypeScriptは 5.9 系が最新ラインで、GitHub Releases では TypeScript 5.9.3(Latest) が 2025-10-01 に出てるよ。 (GitHub)
あと TypeScript 5.9 から tsc --init の初期設定が強めになって、noUncheckedIndexedAccess と exactOptionalPropertyTypes がデフォルトで入るのも “型で事故を減らす” 流れとして大きいよ。 (Microsoft for Developers)
9-1. 今日のゴール🎯💕この章を終えると、こんなことができるようになるよ👇✨
- ✅ 不正な値(例:存在しないステータス、nullっぽい事故)を 型で入れない
- ✅ 「入力チェック」や「状態の組み合わせミス」みたいな テストを減らす
- ✅ I/O境界(外側)で“整形・検証”して、中心(ロジック)をスッキリ保つ🧼🏠
9-2. 「型でテストが減る」ってどういうこと?🧠🪄### 💡減らせるテストの代表例* 「この関数に undefined 渡したらどうなる?」系
- 「ステータスに
delivereddd(typo)入ったら?」系 - 「オプションプロパティがある前提で触って落ちる」系
これ、型が強いと そもそもコードが書けない or コンパイルで怒られる から、テストで守る必要が薄くなるの。
たとえば strictNullChecks を有効にすると、null/undefined を雑に扱うとコンパイルで止めてくれるよ。 (TypeScript)
ただし注意⚠️: “外から来るデータ” は現実では壊れてる可能性があるよね? だから 境界で検証して Domain 型に変換して、中心は「正しい前提で」書くのが王道だよ〜!🚪➡️🏠✨
9-3. 型で「不正」を締め出す4大テク🧰✨### ① リテラル型 + ユニオン型(typo を物理的に消す)
🧯「文字列で状態を持つ」のが一番事故るポイント!😭 まずはこれを “決め打ちの集合” にしちゃう👇
type OrderStatus = "draft" | "paid" | "shipped" | "canceled";
function canShip(status: OrderStatus): boolean {
return status === "paid";
}
// canShip("paidd"); // ❌ typo はコンパイルで死亡👍
✅ これだけで「typo系テスト」はかなり減るよ〜!🎉
② Discriminated Union(状態×データの矛盾を消す)
🧩「ステータスは shipped なのに trackingNumber が無い」みたいな矛盾、あるある…😇 それ、型で “ありえない形” にしちゃお!
type Shipping =
| { kind: "notShipped" }
| { kind: "shipped"; trackingNumber: string };
function label(shipping: Shipping): string {
if (shipping.kind === "shipped") {
return `追跡番号: ${shipping.trackingNumber}`;
}
return "未発送";
}
✅ “状態に必要なデータが揃ってるか” をテストで守る量が減るよ!💕
③ Exhaustive Check(switch漏れをコンパイルで発見)
🕵️♀️「新しい状態を増やしたのに、分岐追加し忘れ」って事故を型で止めるやつ!✨
type PayMethod = "card" | "bank" | "cash";
function fee(method: PayMethod): number {
switch (method) {
case "card":
return 120;
case "bank":
return 80;
case "cash":
return 0;
default: {
const _never: never = method; // ✅ ここが保険
return _never;
}
}
}
✅ ケース追加漏れのテストを “型” が肩代わりしてくれるよ〜!💪✨
④ Brand 型(ID・金額・率の取り違えを消す)
🏷️💎userId と orderId が同じ string だと、取り違え事故が起きるよね…😵💫
Brand 型で「同じstringでも別物」にできる!
declare const userIdBrand: unique symbol;
declare const orderIdBrand: unique symbol;
type UserId = string & { readonly [userIdBrand]: "UserId" };
type OrderId = string & { readonly [orderIdBrand]: "OrderId" };
function toUserId(raw: string): UserId | null {
// 例:簡易チェック
if (!raw.startsWith("u_")) return null;
return raw as UserId;
}
function loadUser(userId: UserId) {
// ...
}
const uid = toUserId("u_123");
if (uid) loadUser(uid);
// loadUser("u_123"); // ❌ 生stringは渡せない
✅ 「取り違え」テストが激減するタイプのやつ!🥳
9-4. 型ガード(Type Guard)
で “境界の検証” をうまく書く🚧✅外から来るデータ(API/フォーム/JSON)は信用しない!🙅♀️ 境界でチェックして Domain 型 に変換してから中心へ渡すと、中心のテストがスリムになるよ✨
type Coupon =
| { kind: "percent"; value: number } // 0〜100
| { kind: "fixed"; value: number }; // 円
function isCoupon(x: unknown): x is Coupon {
if (typeof x !== "object" || x === null) return false;
const obj = x as any;
if (obj.kind === "percent") return typeof obj.value === "number";
if (obj.kind === "fixed") return typeof obj.value === "number";
return false;
}
ポイントは👇
- ✅ 境界:
unknownから安全にCouponへ - ✅ 中心:
Couponが来る前提でロジックが書ける → 中心の「変な入力テスト」が減る!🎉
9-5. “最近の推奨” tsconfig の強化ポイント⚙️
🧊TypeScript 5.9 の tsc --init では、最初からけっこう強い設定が出るよ。
特にこの2つは “事故を減らす” のに効く! (Microsoft for Developers)
✅ noUncheckedIndexedAccess「存在しないかも」なキーアクセスに undefined が混ざるようになる。
つまり “取り出した値がある前提で触る事故” が減る! (TypeScript)
✅ exactOptionalPropertyTypes「optional は optional」と厳密に扱う設定。
地味だけど「optional を雑に扱ってバグる」を減らせるよ。 (TypeScript)
✅ strictNullChecks(strictの一部)null/undefined の扱いが雑だとコンパイルで止まる!
strict を有効にするとデフォルトで true 扱いだよ。 (TypeScript)
例:差分だけ入れるならこんな感じ💡
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true
}
}
9-6. ハンズオン:入力型を強くして “テストを減らす”🚧➡️
💎題材:割引を適用して合計金額を返す🛒🍰
Step 0:ありがちな弱い版(テストが増えるやつ)
😇
function applyDiscount(total: number, discountType: string, value: number): number {
if (discountType === "percent") return Math.floor(total * (1 - value / 100));
if (discountType === "fixed") return Math.max(0, total - value);
return total; // え?unknownなタイプ来たら?😵💫
}
この形だとテストがこう増える👇
- discountType が変な文字列のとき
- percent なのに value が 1000 のとき
- total が負のとき… etc 😱
Step 1:まずユニオンで「タイプのtypo」を消す🧯
type DiscountType = "percent" | "fixed";
Step 2:Discriminated Unionで「組み合わせ矛盾」を消す🧩
type Discount =
| { kind: "percent"; value: number } // 0〜100 を想定
| { kind: "fixed"; value: number }; // 0以上を想定
function applyDiscount(total: number, discount: Discount): number {
switch (discount.kind) {
case "percent":
return Math.floor(total * (1 - discount.value / 100));
case "fixed":
return Math.max(0, total - discount.value);
default: {
const _never: never = discount;
return _never;
}
}
}
✅ これで「unknownなdiscountType来たら?」系のテストが減るよ🎉
Step 3:境界で “検証してから” 中心へ渡す(中心のテストをスリム化)
🚪✨境界で unknown を Discount にしてから中心へ👇
type ParseResult<T> = { ok: true; value: T } | { ok: false; message: string };
function parseDiscount(input: unknown): ParseResult<Discount> {
if (typeof input !== "object" || input === null) return { ok: false, message: "not object" };
const x = input as any;
if (x.kind === "percent" && typeof x.value === "number" && 0 <= x.value && x.value <= 100) {
return { ok: true, value: { kind: "percent", value: x.value } };
}
if (x.kind === "fixed" && typeof x.value === "number" && 0 <= x.value) {
return { ok: true, value: { kind: "fixed", value: x.value } };
}
return { ok: false, message: "invalid discount" };
}
ここが超大事💖
- ✅ 境界のテスト:
parseDiscountに集中(入力のバリエーション多めOK) - ✅ 中心のテスト:
applyDiscountは “正しいDiscountが来る前提” でシンプルに(ケース少なめ)
→ 「中心が変な入力で壊れる系テスト」がごっそり減るよ!🥳
9-7. 章末チェックリスト✅🌸* [ ] 状態や種別は string 直書きじゃなくて ユニオン型 にした?
- 状態によって必要なデータが変わるなら Discriminated Union にした?
-
switchには neverチェック を入れた? - 外から来る値は
unknown→ 境界で検証してDomain型へ にした? -
strict+noUncheckedIndexedAccess+exactOptionalPropertyTypesを意識できた? (Microsoft for Developers)
まとめ🍀
✨型を強くすると…
- 🧯 “そもそも書けないバグ” が増える(最高!)
- 🧪 テストは「中心のロジック」へ集中できる
- 🚪 境界で検証してDomain型に変換できると、中心がどんどん綺麗になる
次の章(第10章)は「テストしにくい臭い」カタログ👃💨だよ! 今日の内容を知ってると、「あ、ここ型と境界で直せるやつだ!」って嗅ぎ分けが上手くなるよ〜😆🫶