第05章:TypeScriptで作る「静的な契約」①:型で守れる範囲🧷✅

5.0 この章でやること(ゴール)🎯✨
TypeScriptの型を使って、DbCの「契約(約束)」のうち、コンパイル時に守れる範囲をしっかり体感します🙂🧠
この章のゴールはこの3つ👇
- 「ありえる値」を型で狭める(
string | null/ union / literal type)🎯 - 「勝手に変えない」を型で宣言する(
readonly)🧊 UserId/Currencyみたいな**ドメインの名札(型)**を作れるようになる🪪💴
5.1 最新メモ(2026-01-25 時点)🗓️🧩
typescriptの npm 最新は 5.9.3(2025-09-30 公開)です📦✨ (npm)- TypeScript 6.0 は「5.9 → 7 への橋渡し(bridge)」として位置づけられ、7.0(ネイティブ化)へ向けた調整が進む方針が示されています🧱➡️🚀 (Microsoft for Developers)
※この章で扱う「union / literal / readonly / satisfies」などは、今の実務でも“ど真ん中”で使われている安定テクです😊✅
5.2 「静的な契約」ってなに?🧠🧷
静的な契約=型で守る約束です✨ 実行する前に(=コンパイル時に)「それ、ありえない値だよね?」って弾けるのが強み💪🛑
たとえば👇
nullが来るかもしれないならstring | nullと書く(来ないなら来ない型にする)🚧- 通貨は
"JPY" | "USD"みたいに“候補だけ”にする💴 - 値を勝手に変えないなら
readonlyにする🧊
つまり、型は「入口で壊れた値を入れない」ための第一関門🚪✨ ただし、数値の範囲とか文字数みたいな「実行時にしか分からない約束」は次の章以降で扱います🌍🛡️
5.3 string | null / union で「ありえる値」を狭める🎯✅
5.3.1 null が来るなら、型に書く📝
null が来る可能性があるのに string とだけ書くと、受け取る側が事故りやすいです😵💫💥
なので「来るなら来る」で正直に書くのが契約です🙂🤝
type DisplayName = string | null;
function greet(name: DisplayName) {
if (name === null) {
return "こんにちは!名無しさん🙂";
}
return `こんにちは、${name}さん😊`;
}
ポイント👇
if (name === null)みたいに分岐すると、TypeScriptがその中では string だねって分かってくれます(これを narrowing と呼びます)🔍✨
5.3.2 union は「選択肢を絞る」最強ワザ⚡
「状態」や「種別」を union にすると、ありえない分岐が減ります🧠✨
type Plan = "FREE" | "PRO" | "ENTERPRISE";
function canExportPdf(plan: Plan) {
return plan === "PRO" || plan === "ENTERPRISE";
}
// ✅ OK
canExportPdf("PRO");
// ❌ ダメ(候補にない)
canExportPdf("VIP");
5.4 literal type と as const で「値をそのまま型にする」🧊✨
5.4.1 そのままだと “広がる” 問題(widening)🌪️
TypeScriptは放っておくと、文字列リテラルを string に“広げる”ことがあります。
const currency = "JPY";
// currency の型は "JPY" ではなく string になることが多い
ここで as const を使うと、「これは変えない前提の固定値だよ🧊」が伝わります!
const currency = "JPY" as const;
// currency の型が "JPY" になる✨
5.4.2 as const はオブジェクトでも強い🧷
const appConfig = {
env: "prod",
retry: 3,
region: "jp",
} as const;
// env は "prod"、retry は 3、region は "jp" の型になる✨
5.5 satisfies で “形だけチェック” して、型は潰さない🧩✅
satisfies は「この形(型)を満たしてる?」をチェックするのに便利です✨
しかも、型注釈みたいに情報を潰しにくいのが良いところ🙂🧠
(satisfies 自体は TypeScript 4.9 で導入されています) (TypeScript)
例:APIのルート定義で「必須キーが揃ってるか」だけ保証したいとき👇
type Routes = Record<string, { method: "GET" | "POST"; path: `/${string}` }>;
const routes = {
getUser: { method: "GET", path: "/users/:id" },
createUser: { method: "POST", path: "/users" },
// typo: { method: "PUT", path: "users" }, // ❌ method が候補外 & path が / で始まらない
} satisfies Routes;
satisfies の気持ちよさ👇
- 「形がOKか」は厳しくチェック✅
- でも
routes.getUser.methodが"GET"のまま残りやすい(便利!)✨
5.6 readonly で「勝手に変えない」を契約にする🧊🔒
5.6.1 まずはプロパティを readonly にする
type User = {
readonly id: string;
readonly name: string;
};
const u: User = { id: "u_1", name: "Mika" };
// ❌ 代入できない(契約違反)
u.name = "Rika";
5.6.2 配列は readonly を忘れがち🍎⚠️
const items: readonly string[] = ["a", "b", "c"];
// ❌ push できない(勝手に変えない契約)
items.push("d");
5.7 演習:UserId と Currency を型で表現してみる🪪💴✨

ここから手を動かそう〜!✍️😆 「静的な契約」の定番は “名札型(ブランディング)” です🧷
5.7.1 UserId を “ただの string” から卒業させる🎓
狙い:
userIdとemailを間違えて渡す事故を、型で止める🛑✨
type Brand<K, T> = K & { readonly __brand: T };
type UserId = Brand<string, "UserId">;
function asUserId(value: string): UserId {
// ここでは「文字列っぽい」しか守れてない(実行時チェックは後の章で強化🛡️)
return value as UserId;
}
// 使う側
function loadUser(userId: UserId) {
return `loading... ${userId}`;
}
const id = asUserId("u_123");
loadUser(id); // ✅ OK
const email = "test@example.com";
// loadUser(email); // ❌ string は UserId じゃない!
✅ ここで起きる良いこと
stringなら何でもOK、が消える- “それっぽい値の混入” をかなり減らせる🎯✨
5.7.2 Currency を union で固定する💴🧷
type Currency = "JPY" | "USD" | "EUR";
type Money = {
readonly amount: number;
readonly currency: Currency;
};
function formatMoney(m: Money) {
return `${m.amount} ${m.currency}`;
}
formatMoney({ amount: 1200, currency: "JPY" }); // ✅
// formatMoney({ amount: 1200, currency: "YEN" }); // ❌ 候補外
5.7.3 ミニ課題(提出物)📮✨
次の3つを作ってみよう🙂✍️(コピペOK、でも理解が最優先!)
ProductIdをUserIdと同じ方式で作る🪪Currencyに"GBP"を追加して、Moneyが普通に動くようにする💷readonlyを付け忘れてる箇所を探して直す🔍🧊
5.8 VS Codeで「型の契約」を目で見るコツ👀✨
型は “読める” ようになると爆速で強くなります🏃♀️💨
- 変数にマウスを乗せて型を確認(ホバー)🖱️👀
- 赤波線が出たら、まずカーソル当ててメッセージを読む🧯
F2(リネーム)で、命名の一貫性を守る🪄Ctrl + .(クイックフィックス)で候補を見る✨
5.9 この章のまとめ🧁✅
- union(
A | B)で「ありえる値」を絞る🎯 - literal type と
as constで「固定値」を契約にする🧊 readonlyで「勝手に変えない」を契約にする🔒UserId/Currencyみたいな “名札型” は、実務で事故を減らす神テク🪪✨- ただし、範囲・文字数・整合性みたいな契約は 型だけでは守れない(次でやるよ🌍🛡️)
5.10 章末チェックリスト✅🎒
-
string | nullを見たら、null分岐を書ける🙂 - union の候補から外れた値が弾かれるのを確認した🛑
-
as constで literal が残るのを確認した🧊 -
readonlyで代入事故が止まるのを確認した🔒 -
UserIdを “ただの string” から分離できた🪪✨
5.11 ミニクイズ🎲🙂
Q1. type A = "x" | "y" に "z" を渡すとどうなる?
Q2. readonly の配列に push しようとしたらどうなる?
Q3. UserId を string と混ぜて使えるようにしちゃうと、何が起きる?😵💫
(答えは:全部「事故を事前に止めるために、止まる!」です😆🛑✨)