第06章:型で守る①(ID・金額・数量)🧱🔒
0. この章でできるようになること 🎯✨
- 「ただの string / number」を卒業して、取り違え事故を型で止められるようになる🙅♀️💥
- OrderId / Money / Quantity を “薄い型(薄いValue Object)” として作れる🧩💎
- 「負の金額」「小数の数量」「別IDの混入」みたいな事故を、なるべく早く(コンパイル時 or 生成時) に叩ける🔨⚡
1. まず、事故のイメージをつかむ 😱🧨
1-1. string地獄:IDの取り違え🙃

-
OrderId も ProductId も UserId も、全部 string のままだと…
- 「注文IDのつもりで商品IDを渡す」みたいな事故が起きる💥
- 動いてしまって、気づくのが遅いのが一番こわい🫠
1-2. number地獄:金額と数量のやらかし💸📦
-
Money(お金)も Quantity(数量)も、number のままだと…
-100円が入る😱0.1個みたいな “ありえない数量” が入る😵💫(商品によってはNG)- 「金額」と「個数」を足し算しちゃう…とか(泣)😭
だから、この章では 「意味の違い」を型で分けるよ!🛡️✨
2. 方針:TypeScriptで “薄いValue Object” を作る 💎🪶
2-1. 2つの守り方(どっちも使う)🧠✨
-
ブランド型(Branded Type)
- 中身は string / number のままだけど、型だけ別物にする🪪
- TypeScriptで「擬似的に名前的型(nominalっぽく)」を作る定番パターンだよ📌 (GitHub)
-
生成関数(Factory)でバリデーション
- 「空文字は禁止」「金額は0以上」「数量は整数」などを 生成時にチェック ✅
- これで “不正な状態を作らない” に近づく🔒✨
3. 実装:まずは共通の Brand 型を作る 🧩🪪
src/domain/shared/brand.ts を作ろう📁✨
// src/domain/shared/brand.ts
// いわゆる「ブランド型」用の共通ユーティリティ 🪪✨
// T = 元の型(string/numberなど)
// B = 目印(ブランド)
// これで「見た目が同じでも別の型」にできるよ!
export type Brand<T, B> = T & { readonly __brand: B };
💡 ここでは分かりやすさ優先で
__brandを使うよ! より厳密に「ブランド名衝突を避けたい」ならunique symbolを使うやり方も定番🧠✨ (DEV Community)
4. 実装:OrderId(IDは “ただの文字列” じゃない)🆔🧱
src/domain/order/orderId.ts を作ろう📁✨
// src/domain/order/orderId.ts
import { Brand } from "../shared/brand";
type OrderIdBrand = "OrderId";
export type OrderId = Brand<string, OrderIdBrand>;
export const OrderId = {
create(raw: string): OrderId {
const trimmed = raw.trim();
// ✅ 生成時にルールで守る
if (trimmed.length === 0) {
throw new Error("OrderId は空にできません");
}
// 例:見分けやすいprefixを付ける(任意)
// ord_ から始まる、など
if (!trimmed.startsWith("ord_")) {
throw new Error("OrderId は 'ord_' で始めてね");
}
return trimmed as OrderId;
},
// 「これ OrderId っぽい?」を判定したい時のガード(任意)👀✨
is(raw: string): raw is OrderId {
return raw.startsWith("ord_") && raw.trim().length > 4;
},
} as const;
4-1. これで何が嬉しいの?🎁✨
OrderIdとProductIdを 同じ string として扱えなくなる- 「引数を間違えた」だけで コンパイル時に止まる(理想)🛑💕
5. 実装:Money(金額は “事故りやすい” 代表)💴💥
お金は 小数が混ざると地獄なので、まずは教材として「円=整数」として扱うよ🧾✨
src/domain/shared/money.ts を作ろう📁
// src/domain/shared/money.ts
import { Brand } from "./brand";
type MoneyBrand = "Money";
export type Money = Brand<number, MoneyBrand>;
export const Money = {
yen(amount: number): Money {
// ✅ ルール(必要最低限)
if (!Number.isFinite(amount)) throw new Error("金額が数値じゃないよ");
if (!Number.isInteger(amount)) throw new Error("円は整数で扱おう(小数NG)");
if (amount < 0) throw new Error("金額はマイナス禁止だよ");
return amount as Money;
},
add(a: Money, b: Money): Money {
return (a + b) as Money;
},
mul(a: Money, n: number): Money {
if (!Number.isInteger(n)) throw new Error("個数は整数でね");
if (n < 0) throw new Error("個数はマイナス禁止");
return (a * n) as Money;
},
toNumber(m: Money): number {
return m as number;
},
} as const;
5-1. “型で守る” と “生成で守る” の合わせ技 🧠🛡️
- 型だけだと、
-100 as Moneyみたいな 雑な型アサーションで突破できちゃう😇 - だから 基本は Money.yen(...) を通して作る、が大事✅✨ (「不正な値の入口」を狭めるのがコツだよ🚪🔒)
6. 実装:Quantity(数量は “0以上の整数” にしたい)📦🔢
src/domain/shared/quantity.ts を作ろう📁✨
// src/domain/shared/quantity.ts
import { Brand } from "./brand";
type QuantityBrand = "Quantity";
export type Quantity = Brand<number, QuantityBrand>;
export const Quantity = {
of(n: number): Quantity {
if (!Number.isFinite(n)) throw new Error("数量が数値じゃないよ");
if (!Number.isInteger(n)) throw new Error("数量は整数でね");
if (n <= 0) throw new Error("数量は1以上にしてね");
return n as Quantity;
},
toNumber(q: Quantity): number {
return q as number;
},
} as const;
😊 商品によっては「0個OK(カートに入れない)」とかもあるから、ここはドメインに合わせて調整してOK!
7. 使ってみる:OrderItemで “金額×数量=小計” を安全にする 🧾✨
src/domain/order/orderItem.ts
// src/domain/order/orderItem.ts
import { Money } from "../shared/money";
import { Quantity } from "../shared/quantity";
type ProductIdBrand = "ProductId";
type ProductId = string & { readonly __brand: ProductIdBrand };
const ProductId = {
create(raw: string): ProductId {
const t = raw.trim();
if (t.length === 0) throw new Error("ProductId は空にできません");
if (!t.startsWith("prd_")) throw new Error("ProductId は 'prd_' で始めてね");
return t as ProductId;
},
} as const;
export type OrderItem = {
productId: ProductId;
unitPrice: ReturnType<typeof Money.yen>;
quantity: ReturnType<typeof Quantity.of>;
};
export const OrderItem = {
create(productId: ProductId, unitPrice: ReturnType<typeof Money.yen>, quantity: ReturnType<typeof Quantity.of>): OrderItem {
return { productId, unitPrice, quantity };
},
subtotal(item: OrderItem) {
return Money.mul(item.unitPrice, Quantity.toNumber(item.quantity));
},
} as const;
7-1. “間違った計算” を型で止めるイメージ🛑🧠
Money.add(単価, 数量)みたいな変な計算をしにくくなる🙅♀️- 「全部 number の世界」より、意図がハッキリして読みやすい📖✨
8. satisfies で “表(テーブル)” を安全にする 📋✅
たとえば「商品コード → 単価」の表を作るとき、値が Money になってるかをチェックしたいよね👀✨
satisfies は「型に合ってるかチェックするけど、推論は潰さない」便利な演算子だよ🪄 (typescriptlang.org)
import { Money } from "../shared/money";
const priceTable = {
coffee: Money.yen(480),
tea: Money.yen(420),
// cola: 200, // ← これは Money じゃないから弾けるようにしたい!😤
} satisfies Record<string, ReturnType<typeof Money.yen>>;
9. “型が効かない” ときのチェック(VS Codeあるある)🧯💻
「なんか補完やエラー表示が変…?」って時は、VS Code が ワークスペースの TypeScript を使ってないことがあるよ🌀 VS Code では “ワークスペースのTypeScriptを使う” に切り替えできる(コマンドで選ぶ)って公式ドキュメントにもあるよ📌 (Visual Studio Code)
10. 練習問題(手を動かすターン)📝🔥
練習1:VO候補を増やす💎
次の候補を、Brand+create で作ってみよう🎮✨
- Address(住所)🏠
- Email(メール)📧
- Percent(割引率 0〜100)🏷️
- DateRange(開始〜終了)📅
練習2:コンパイルで止めたい事故を書いてみる🧨
「これは本当はダメ」をわざと書いて、TypeScriptに怒らせよう😈⚡
- OrderId と ProductId を入れ替える
- Money に小数を入れる
- Quantity に 0 や -1 を入れる
11. AI活用(この章向け)🤖✨
11-1. VO候補を洗い出すプロンプト例🧠💡
- 「ミニECのドメインで、string/numberのままだと事故りそうな値を Value Object 候補として20個出して。理由も一言で。」
- 「Money/Quantity/Id系でよくあるバリデーションルールを、実務っぽい観点で10個出して。」
11-2. “やりすぎ” を止めるプロンプト例🛑🧸
- 「このValue Object設計、初心者教材として複雑すぎる点ある?削れるなら削って提案して。」
12. まとめ 🌸🏁
- ID・金額・数量は、いちばん事故りやすいから、まず型で守るのがコスパ最強🛡️✨
- ブランド型+生成関数で、「意味の違い」と「不正な値」を早めに止められる🙅♀️✅
- これは後で出てくる 集約の不変条件🔒 を守る土台にもなるよ🧱💕
参考(本章で使った “最新” のよりどころ)📌🗓️
- 2026年1月下旬時点、公式ドキュメントの最新リリースノートは TypeScript 5.9。 (typescriptlang.org)
satisfiesは「型チェックしつつ推論を保つ」ための機能として公式リリースノートに説明があるよ。 (typescriptlang.org)- ブランド型(Branded Types)はTypeScriptで“取り違え”を防ぐ定番パターン。 (GitHub)