■ 1. 概要・前提とゴール
- 発表者: 梶川 琢馬 (@kajitack) - 株式会社TechBowl VPoT / TechTrain開発・メンター
- 発表イベント: フロントエンド・PHPカンファレンス北海道2026
- 対象者: TypeScriptおよびPHPを使う開発者で、「代数的データ型」を知らない方
- ゴール: 代数的データ型の雰囲気を掴み、フロントエンドのUI構造とPHPでのエラーハンドリングを代数的データ型で表現する方法を理解する
■ 2. 型に対する考え方
- 型は「ビット列の解釈」としてではなく「集合」として捉える
- ビット列の解釈: データのサイズや扱い方を意識した型定義
- 集合: 「取りうる値はこれだけ」と値の範囲を決め、実行前に処理を判断するための型定義
- TypeScript公式でも「Types as Sets」として紹介されている
- 型を書くこと = 集合を絞ること
- 例: string型(ありえない値の組み合わせが膨大)→ OrderStatus型("pending" | "paid" | "shipped")でドメイン知識により絞り込む
- 型(集合)を厳密に宣言することの利点:
- 値の範囲を厳密に決めることで、ありえない状態を未然に防ぐ
- 実行時にチェックする防御的プログラミングが不要になる
- 補完やガードレールとしての開発者体験が向上する
- テストの記述量を削減できる
- 設計方針: ドメインの知識で型を絞り「出来ていいことだけを出来るようにする」(t-wada「予防に勝る防御なし」より)
■ 3. 代数的データ型 (ADT: Algebraic Data Types)
- 掛け算や足し算のように型を組み立てる概念
- 積(AND)型:
- 直積型(Product Type): 複数の型の値を同時に保持する
- 代表例: Class / Struct / オブジェクト型 / タプル
- 値の数は「各フィールドの値の数の掛け算」で増える(例: name × age)
- 和(OR)型:
- 列挙型(Enumerated Type): 複数の値の選択肢のうち1つをとる、ただし構造(直積)を持てない
- 直和型(Sum Type): 直積を持てる列挙型。代表例: Discriminated Union / Tagged Union / Sealed クラス
- 値の数は「各ケースの値の数の足し算」で増える
kindのような共通フィールドが判別子(タグ)となり、その値でケースを見分け型が絞り込まれる- データの仕様は突き詰めると「かつ」と「または」でできている
- 特に和(OR)が型を組み合わせる際に強力な手段となる
- 継承と直和型の違い:
- 継承: 親が子を知らないため、集合で「全部で何通りか」が確定しない(開いている)
- 直和型(Sealed クラス): 型で子を決めておくことができるため集合が閉じる
- Make Illegal States Unrepresentable: 仕様上ありえない状態は表現しない、コンパイラや静的解析がそれを理解する
■ 4. 実践編: フロントエンド - UIの状態とデータのモデリング
- データ取得状態を boolean/null の積(直積)で管理する問題:
- isLoading / error / data の3つのフラグで 2×2×2 = 8通りの組み合わせが発生する
- 有効な状態は「未取得」「読み込み中」「エラー」「取得済み」の4通りのみ
- 「読み込み中なのにエラーもデータもある」などの不正な状態が表現可能になってしまう
- boolean型はたいてい「別の型」の成れの果て("That boolean should probably be something else"):
isConfirmed: boolean→confirmedAt: Date | null(一度きりの出来事の情報を保持)isAdmin: boolean→role: "admin" | "editor" | "viewer"(状態が増えても網羅チェックが効く)check(user): boolean→Allowed | NotPermitted(reason)(boolean blindnessの回避)- UIの状態を Discriminated Union で表現:
- useState 3本の直積をやめ、直和1本(AsyncData型)で持つ
AsyncData = { kind: "notAsked" } | { kind: "loading" } | { kind: "success"; data: Data } | { kind: "error"; error: Error }- 取りうる4状態のみをぴったり表現できる
- never を使った網羅チェック(Exhaustiveness check)パターン:
- switch文のdefaultで
const _exhaustiveCheck: never = stateとすることで、ケース漏れを型エラーとして検出する- 構造が異なるデータの集まりを Discriminated Union で表現:
- 例: チャットメッセージ(botText / userText / tagSelect / mentorCards)を1つのChatMessage型で表現
- 各ケース固有のプロパティへの誤アクセスを型エラーとして防止できる
- 直和の中に直和を入れ子にして複雑な状態も表現できる
- 描画ロジックでも同じ網羅チェックが効く:
- switch文で各ケースのコンポーネントを返す構造にすることで、種別追加時の描画コード修正漏れをコンパイラが検出する
- フロントエンド編まとめ:
- UIの状態をフラグ(直積)で持つとありえない状態が混ざる
- 状態も、構造が異なる要素の集まりも、直和(判別可能ユニオン)で列挙する
- exhaustive check で網羅的な型チェックが可能になる
■ 5. 実践編: PHP - ドメインルールと整合性を守る
- エラーもドメインの一部としてモデル化する(「関数型ドメインモデリング」より):
- ドメインをモデル化する際はプリミティブ型を使わず、ドメインに特化した型を作成する
- エラーも同様に扱い、特別な対応が必要なエラーの種類ごとに個別のケースを用意する
- 例外は型シグネチャに現れない問題:
- 例外は副作用であり、実行するまで何が起きるか分からない
- 処理の失敗は「例外」ではなく、成功と同様に考慮すべき「結果」として扱う
- Result型:
- 成功か失敗か、どちらか一方を表す直和型
Result<T, E> = Ok(T) | Err(E)(成功なら Ok が値 T を持ち、失敗なら Err がエラー E を持つ)- 失敗を「投げる」のではなく、値として「返す」
- PHPには標準でResult型がない
- PHPでのResult型実装:
- Sealed クラス(直和型)を PHPStan(静的解析)の
@phpstan-sealedアノテーションで実現interface Resultに@phpstan-sealed Ok|Errを付与し実装を特定クラスのみに限定するfinal readonly class Ok implements Resultとfinal readonly class Err implements Resultで構成- Result型を使うとエラー分岐も網羅できる:
- match式でエラーケース(OrderError::UserNotFound, OrderError::OutOfStock)を分岐する
- ケース漏れはPHPStanが検出する
- 失敗が「ビジネスロジックの値」になる
- PHPのADT推進の動き:
- RFC が進行中(Enumerations, Tagged Unions, Pattern Matching "is" keyword など段階的に実装予定)
- enumはその一歩であり、Result型や直和型がいつかサポートされる可能性がある
- PHP編まとめ:
- 想定できる失敗は「例外」ではなく、Result型で値として返す
- データ付きの直和は Sealed クラスで作り、静的解析で閉じて絞り込む
- エラー分岐は match で網羅し、ケース漏れは静的解析が検出する
- ADT の RFC が進行中であり、PHPの型アップデートに注目
■ 6. 全体まとめ
- 型を「集合」として捉えると設計の武器になる
- 厳密な型は補完・静的解析・AIが読み取ってくれる
- 代数的データ型 = 「直積」と「直和」で組み立てる型
- 不正な状態を「存在させない」型定義ができる
- 直和型はケースごとに異なる構造を持てるので強力
- UIの状態管理もドメインルールも同じ考え方で守れる
- さらなる発展として、ジェネリクス(PHP 8.6)や型理論(述語や値でさらに集合を絞り込む世界)にも注目できる