Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Make Illegal States Unrepresentable

Avatar for ONDA, Takashi ONDA, Takashi
February 24, 2026
31

Make Illegal States Unrepresentable

2026-02-24 TSKaigi Mashup Kansai のスライドです

Avatar for ONDA, Takashi

ONDA, Takashi

February 24, 2026
Tweet

Transcript

  1. 2 自己紹介 京都在住 一休レストランの開発 最近はバックエンドの Rust がお仕事の中心 20 年の歴史があるプロダクト 3

    年前からリニューアル フロントエンド + 検索・表示系は完了 今はドメインの根幹ロジックを移植中 株式会社一休 恩田 崇
  2. 3 Rust での経験 型が強い言語 ドメインモデルを型で表現したコードベース コンパイルが通れば正しさが担保される // NG fn place_order(

    customer_id: String, product_id: String, quantity: i32, ) // OK fn place_order( customer: VerifiedCustomer, product: ProductId, qty: OrderQuantity, ) -> PlacedOrder
  3. 5 Constraining the solution space "increasing trust and reliability required

    constraining the solution space: specific architectural patterns, enforced boundaries, standardized structures" — Böckeler, Harness Engineering (martinfowler.com, 2026-02-17) ハーネスエンジニアリング: OpenAI が 2026 年2 月に提唱 Böckeler が本質を 解空間の制約 と表現 3 つの柱: Context Engineering: プロンプト、ドキュメント構造 Architectural Constraints: 型制約、リンター ← ガードレール Garbage Collection: 逸脱の検出・修正 型は Architectural Constraints の最も構造的な実装
  4. 6 Design by Contract Bertrand Meyer, Object-Oriented Software Construction, Prentice

    Hall, 1988 precondition / postcondition / invariant で契約を定義 契約を満たさない呼び出しはプログラムの責任
  5. 7 Domain Modeling Made Functional Scott Wlaschin, Domain Modeling Made

    Functional, Pragmatic Bookshelf, 2018 make illegal states unrepresentable 型で不正な値を排除すれば、検証コードが不要になる
  6. 8 異なる時代、同じ結論 時代も背景も違うのに辿り着いた結論は同じ DbC (OOP / runtime) DMMF (FP /

    compile time) Entry precondition parse, don’t validate Exit postcondition return type Preservation class invariant + assertion immutable data parse, don’t validate (Alexis King, 2019): validate: boolean を返す。結果が型に残らない parse: unknown → 型付き値に変換。検証結果が型として残る ` ` ` `
  7. 10 型の性質を活かすには 型チェックは型と型の関係を確認している 型で表現しない = 実行時チェックか暗黙の前提に頼る 型で表現する = 全称命題になり、コンパイラが検査する //

    string: 実行時チェックが必要 function send(to: string) { if (!isEmail(to)) throw new Error(...) // ... } // Email: チェック不要 function send(to: Email) { // そのまま使える }
  8. 11 TypeScript で使える道具 nominal type: 構造が同じでも区別する branded type (振る舞いなし) class

    + #private (振る舞いあり) discriminated union: 状態を絞り込む ステートマシン: 状態遷移を型で表現する
  9. 12 branded type import { z } from "zod"; //

    branded type: 振る舞いを持たない単純な値 const Email = z.string().email().brand<"Email">(); type Email = z.infer<typeof Email>; const UserId = z.string().uuid().brand<"UserId">(); type UserId = z.infer<typeof UserId>; // parse: unknown → Email // validate ではなく parse — 型に検証結果を刻む const email: Email = Email.parse(input); // コンパイルエラー: Email と UserId は別の型 declare function sendTo(email: Email): void; sendTo(userId); // ❌ Type 'UserId' is not assignable to type 'Email'
  10. 13 class + #private field #private field で plain object

    と型レベルで区別される(nominal type ) コンストラクタ引数の型が前のステップを強制する メソッドの戻り値型が次のステップを示す ` ` class Cart { #items: ReadonlyArray<Item>; constructor(items: ReadonlyArray<Item>) { this.#items = items; } confirm(): ConfirmedOrder { return new ConfirmedOrder(this.#items); } } ` `
  11. 14 discriminated union 直積型は optional フィールドで invalid な組み合わせを許してしまう discriminated union

    なら各バリアントに valid なフィールドだけ持たせられる 直和でモデリングすることを第一選択肢にする // ❌ 直積型: invalid な状態が住み着く type OrderBad = { status: "cart" | "confirmed" | "shipped"; confirmedAt?: Date; // cart なのに confirmedAt がある? trackingNumber?: string; // confirmed なのに trackingNumber がある? }; // ✅ 直和型: バリアントごとに valid なフィールドだけ type Order = | { status: "cart"; items: Item[] } | { status: "confirmed"; items: Item[]; confirmedAt: Date } | { status: "shipped"; items: Item[]; confirmedAt: Date; trackingNumber: string };
  12. 15 class + discriminated union 各バリアントを class にして union を構成する

    class の振る舞い + union の網羅性チェックが両立する class Cart { readonly status = "cart" as const; #items: ReadonlyArray<Item>; confirm(): ConfirmedOrder { return new ConfirmedOrder(this.#items); } } class ConfirmedOrder { readonly status = "confirmed" as const; #items: ReadonlyArray<Item>; #confirmedAt: Date; ship(addr: VerifiedAddress): ShippedOrder { /* ... */ } } class ShippedOrder { readonly status = "shipped" as const; } type Order = Cart | ConfirmedOrder | ShippedOrder;
  13. 16 フロントエンドとステートマシン UI 側で状態を絞り込まないとメソッドが呼べない フロントエンドではデータがメモリに残り、様々な UI 操作を受けて状態遷移し続ける function ConfirmButton() {

    const order = use(OrderContext); return ( <button disabled={order.status !== "cart"} onClick={() => { if (order.status === "cart") order.confirm(); }} > 注文確定 </button> ); }
  14. 17 ステートマシンの実装 class Cart { readonly status = "cart" as

    const; // すべてのバリアントが同じ on() でイベントを受け取る on(e: OrderEvent): Cart | ConfirmedOrder { if (e.type === "confirm" && this.#items.length > 0) return new ConfirmedOrder(this.#items); return this; } } class ConfirmedOrder { readonly status = "confirmed" as const; // 同じシグネチャ。戻り値型だけが異なる on(e: OrderEvent): ConfirmedOrder | ShippedOrder { if (e.type === "ship") return new ShippedOrder(this, e.address); return this; } } // UI は状態を気にせず on() にイベントを渡すだけ order.on({ type: "confirm" });
  15. 19 なぜ今、型駆動設計か 型で不正な状態を排除する… 原理と しては正しい でも、このコードが型の数だけ必要 → 現実には妥協していた コーディングエージェントがこのコ ストを吸収する

    class TaxExcludedPrice { readonly #value: number; private constructor(value: number) { this.#value = value; } static parse(n: number): TaxExcludedPrice { if (n < 0 || !Number.isInteger(n)) throw new Error("invalid"); return new TaxExcludedPrice(n); } add(other: TaxExcludedPrice): TaxExcludedPrice { return new TaxExcludedPrice(this.#value + other.#value); } withTax(rate: TaxRate): TaxIncludedPrice { return new TaxIncludedPrice(this, rate); } valueOf(): number { return this.#value; } toString(): string { return `¥${this.#value.toLocaleString()}( 税抜)` } toJSON(): number { return this.#value; } }
  16. 20 エージェントとのワークフロー プランモードで型グラフを設計する どんな型が必要か、どう接続するかをエージェントと議論する 時間の8 割はここに使う エージェントが型の制約内で実装する 型を定義した時点でガードレールは完成している 不正な状態遷移はコンパイルエラーになる セルフコードレビューで設計を磨く

    コードにして初めて気づく点がある。人間のコーディングと同じ 違和感を拾い、設計を見直す このサイクルで育てたコードベースが最良のガードレール LLM は既存コードをコンテキストとして読む 型で設計されたコードベースなら、自然とそれに倣ったプランが生まれる
  17. 23 なぜクラスか メソッドなら . で補完が効き、Go to Definition で操作が集約される 自由関数だと Search

    References で探す必要がある Go to Definition = O(1) で確定的 Search References = O(n) でフィルタリングが必要 TS コミュニティではクラスを避ける潮流があるが、ドメインモデルの表現にはクラスが自然 ` `