Slide 1

Slide 1 text

No content

Slide 2

Slide 2 text

梶川 琢馬 𝕏 @kajitack 株式会社 TechBowl VPoT TechTrain の開発 / メンター PHP カンファレンスは多数参加 初フロントエンドカンファレンス 初北海道 X でスライド公開してます! x.com/kajitack 2/41

Slide 3

Slide 3 text

No content

Slide 4

Slide 4 text

前提とゴール 前提 フロントエンド (TypeScript) と PHP 「代数的データ型」知らない方向け (意外と使ってるかも) ゴール 代数的データ型の雰囲気を感じてもらう フロントエンドの UI の構造、PHP でのエラーハンドリングを 代数的データ型で書いてみる 4/41

Slide 5

Slide 5 text

「型なんて、正直めんどくさい」 そんなふうに考えていた時期が俺にもありました 5/41

Slide 6

Slide 6 text

型に対する考え方 最近はフロントエンドを TS で書く。PHP も型が強化された。 向き合い方が変わった。 「ビット列の解釈」としてだけでなく、「集合」として捉える ビット列の解釈 文字列や数字、配列などデータの扱い方やデータの サイズを意識した型定義 集合 bool → { true, false } string → { "", "a", ... } 「取りうる値はこれだけ」と値の範囲を決め、 どういう入力・処理なのかを実行せずに判断する ための型定義 TS 公式でも Types as Sets と紹介 https://www.typescriptlang.org/docs/handbook/typescript-in-5-minutes-oop.html#types-as-sets 6/41

Slide 7

Slide 7 text

型を書くことは、集合を絞ること string のまま "pending" "paid" "shipped" "hoge" "" "payed" "PAID " "発送済み?" ありえない値の組み合わせが膨⼤ ドメインの知識で 絞る OrderStatus 型 "pending" "paid" "shipped" ありえない値は最初から⼊らない 7/41

Slide 8

Slide 8 text

予防に勝る防御なし ドメインの知識で型を絞り、 「出来ていいことだけを出来るようにする」 テストだけに頼らず、型と解析ツールに品質を担保させる t-wada「予防に勝る防御なし(2025年版) https://speakerdeck.com/twada/growing-reliable-code-php-conference-fukuoka-2025 8/41

Slide 9

Slide 9 text

型(集合)を厳密に宣言したい! 値の範囲を厳密に決めることで、ありえない状態を未然に防ぐ 実行時にチェックする防御的プログラミングをしなくて済む 補完やガードレールとしての開発者体験の向上 テストの記述量を減らせる さらに型と型を組み合わせてより厳密にしたい 9/41

Slide 10

Slide 10 text

代数的データ型 (ADT:Algebraic Data Types) 掛け算や足し算のように型を組み立てる 積(AND) 直積型(Class / Struct) 和(OR) 列挙型(Enum / Union) 直和型(Discriminated Union / Tagged Union) データの仕様は突き詰めると「かつ」と「または」でできている 特に和(OR)が型を組み合わせる時に強力な武器になる 10/41

Slide 11

Slide 11 text

フロントエンド・PHP カンファレンスで積と和 積(AND)で表すと... 2 × 2 = 4 通り。 isFE = false; isPHP = false は 防ぎたい 和(OR)なら... 1 + 1 + 1 = 3 通り。「どちらでもない」は そもそもありえない type Topic = {isFE: boolean; isPHP: boolean;} TypeScript type Topic = "FE" | "PHP" | "両方"; TypeScript 11/41

Slide 12

Slide 12 text

直積型(Product Type) 複数の型の値を、同時に保持する 代表例: オブジェクト型・class・struct・タプル 値の数は「名前の数 × 年齢の数」。掛け算で増えるから「直積」 // 直積: 名前 かつ 年齢、両方を持つ type User = { name: string; age: number; }; TypeScript class User { public function __construct(public string $name, public int $age) {} } PHP 12/41

Slide 13

Slide 13 text

列挙型(Enumerated Type) 複数の値の選択肢のうち、どれか 1 つになる。ただし、構造(直積)を持てない。 代表例: enum・union (言語によっては直和型の場合もある) 値の数は 1 + 1 + 1 = 3 通り。足し算で増える。 直和型の土台のような型 // 列挙: 赤・黄・青の、どれか1つ type Color = "red" | "yellow" | "blue"; TypeScript enum Color { case Red; case Yellow; case Blue; } PHP 13/41

Slide 14

Slide 14 text

直和型(Sum Type) 直積を持てる列挙型。一番重要。 代表例: 判別可能ユニオン(Discriminated Union)、Sealed クラス kind のような共通フィールドが判別子(タグ)。その値でどのケースかを見分け、型が 絞り込まれる 値の数は「成功の数 + 失敗の数」。和の各ケースの中身が直積 // 直和: 成功(金額つき) または 失敗(エラーつき) type PaymentResult = | { kind: "success"; amount: number } | { kind: "failure"; error: string }; TypeScript 14/41

Slide 15

Slide 15 text

継承と直和型の違い 継承は親が子を知らないので、集合で「全部で何通りか」が確定していない Sealed クラスの場合は、型で子を決めておくことが出来るので直和型となる PHP では静的解析を使って実現する(後述) // 継承は開いている(親は子を知らない) interface PaymentResult { /* ... */ } // 別ファイル・別パッケージの、知らないところで集合が増えるかも? class Refunded implements PaymentResult { /* ... */ } PHP 15/41

Slide 16

Slide 16 text

何が嬉しいのか? Make Illegal States Unrepresentable 仕様上あり得ない状態は表現しない。そしてコンパイラや静的解析がそれを理解する 特に直和型はケースごとに違う構造を持てるので強力 // 直積: 2 × 2 = 4 通り。「両方 null」「両方あり」が作れてしまう type Session = { loginUser: User | null; // ログイン済みなら入る guestToken: string | null; // ゲストなら入る }; TypeScript // 直和: 1 + 1 = 2 通り。仕様通りの状態しか存在しない type Session = | { kind: "loggedIn"; user: User } | { kind: "guest"; token: string }; TypeScript https://functional-architecture.org/make_illegal_states_unrepresentable/ 16/41

Slide 17

Slide 17 text

実践編 代数的データ型を使う フロントエンドの UI の状態とデータのモデリング PHP(バックエンド)のドメインルールと整合性を守る 17/41

Slide 18

Slide 18 text

UI の状態とデータのモデリング フロントエンド編 18/41

Slide 19

Slide 19 text

データ取得中の画面、よく作るやつ https://example.com/users Loading... 19/41

Slide 20

Slide 20 text

boolean や nullを積の組み合わせで管理する場合 「読み込み中なのにエラーもデータもある」が作れてしまう const [isLoading, setLoading] = useState(false); const [error, setError] = useState(null); const [data, setData] = useState(null); TypeScript // バラバラの useState も、まとめて見ればこの直積型 type FetchState = { isLoading: boolean; error: Error | null; data: Data | null; }; TypeScript 20/41

Slide 21

Slide 21 text

2 × 2 × 2 = 8 通り。だが有効なのは 4 つだけ isLoading error data 意味 false なし なし 未取得 true なし なし 読み込み中 false あり なし エラー false なし あり 取得済み true あり なし 読み込み中なのにエラー? true なし あり 読み込み中なのにデータ? false あり あり エラーなのにデータ? true あり あり 全部同時? 21/41

Slide 22

Slide 22 text

That boolean should probably be something else boolean はたいてい「別の型」の成れの果て 一度きりの出来事 — 「いつ起きたか」の情報を捨てない isConfirmed: boolean → confirmedAt: Date | null 種別・ステータス — 状態が増えても網羅チェックが効く isAdmin: boolean → role: "admin" | "editor" | "viewer" 判定結果 — true の意味が型から読めない(boolean blindness) check(user): boolean → Allowed | NotPermitted(reason) 出典: That boolean should probably be something else https://ntietz.com/blog/that-boolean-should-probably-be-something-else/ 22/41

Slide 23

Slide 23 text

UI の状態をDiscriminated Unionで表す useState 3 本の直積をやめ、直和 1 本で持つ。取りうる 4 状態だけを、ぴったり表せる type AsyncData = | { kind: "notAsked" } | { kind: "loading" } | { kind: "success"; data: Data } | { kind: "error"; error: Error }; const [state, setState] = useState({ kind: "notAsked" }); TypeScript 23/41

Slide 24

Slide 24 text

網羅的な型チェックができる never を使った網羅チェック(Exhaustiveness check)パターン switch (state.kind) { case "notAsked": case "loading": return null; case "error": return ; case "success": return ; default: const _exhaustiveCheck: never = state; // 抜けがあれば型エラー return _exhaustiveCheck; } TSX https://typescriptbook.jp/reference/statements/never#neverを使った網羅性チェック 24/41

Slide 25

Slide 25 text

構造が違うデータの集まり メンターレコメンド チャット 学びたい技術を教えてください! type: "botText" React Go 設計 type: "tagSelect" type: "userText" React を学びたいです! おすすめのメンターはこちらです React Next.js React TS type: "mentorCards" 25/41

Slide 26

Slide 26 text

構造が異なるコンポーネントを判別可能ユニオンで表現 botText の処理中に mentors へアクセス → 型エラー。種類ごとの固有プロパティを 取り違えない レコメンドの計算待ちは { type: "mentorCards"; recommend: AsyncData } のように、直和の 中に直和を入れ子にして表せる type ChatMessage = | { type: "botText"; content: string } | { type: "userText"; content: string } | { type: "tagSelect"; tags: Tag[] } | { type: "mentorCards"; mentors: Mentor[] }; const messages: ChatMessage[] = [/* 会話の全メッセージ */]; TypeScript 26/41

Slide 27

Slide 27 text

描画も、状態と同じ網羅チェックが効く 次にどの種別が来るかは実行時に決まる。それでも、来うる種別は直和で閉じている 種別を増やしたら、直すべき描画コードがコンパイラが全部教えてくれる switch (message.type) { case "botText": return ; case "userText": return ; case "tagSelect": return ; case "mentorCards": return ; default: const _exhaustiveCheck: never = message; // 種別の追加漏れは型エラー return _exhaustiveCheck; } TSX 27/41

Slide 28

Slide 28 text

フロントエンド編のまとめ UI の状態をフラグ(直積)で持つと、あり得ない状態が混ざる 状態も、構造が異なる要素の集まりも、直和(判別可能ユニオン)で列挙する exhaustive check で網羅的な型チェック 28/41

Slide 29

Slide 29 text

ドメインルールと整合性を守る PHP 編 29/41

Slide 30

Slide 30 text

エラーもドメインの一部としてモデル化する ドメインをモデル化する際には、文字列などのプリミティブ型を 使わず、ドメインに特化した型を作成します。 さて、エラーも同じように扱われるべきです。 ドメインに 関する議論で特定の種類のエラーが挙がった場合、ドメイン内の 他の要素と同様にモデル化するべきです。 特別な対応が必要なエラーの種類ごとに個別の ケースを用意する 関数型ドメインモデリング ドメイン駆動設計とF#でソフトウェアの複雑さに立ち向かおう Scott Wlaschin (著), 猪股 健太郎 (著) 30/41

Slide 31

Slide 31 text

例外は、型シグネチャに現れない 例外は副作用で、何が起きるか実行するまで分からない 処理の失敗は「例外」ではなく、成功と同様に考慮すべき「結果」 // 例外を投げるかも? public function placeOrder(OrderInput $input): Order PHP /** @return Result ← 失敗の可能性が型に現れる */ public function placeOrder(OrderInput $input): Result PHP 31/41

Slide 32

Slide 32 text

Result 型とは 成功か失敗か、どちらか一方を表す直和型 成功なら Ok が値 T を持ち、失敗なら Err がエラー E を持つ 失敗を「投げる」のではなく、値として「返す」 ただし、PHP にはない Result = Ok(T) | Err(E) 32/41

Slide 33

Slide 33 text

PHP で Result 型を実装する Sealed クラス (直和型) を、PHPStan(静的解析)で実現 /** * @phpstan-sealed Ok|Err (実装を特定のクラスだけに限定する) */ interface Result { public function isOk(): bool; public function unwrap(): mixed; } final readonly class Ok implements Result { /* 値 T を持つ */ } final readonly class Err implements Result { /* エラー E を持つ */ } PHP https://github.com/valbeat/php-result 33/41

Slide 34

Slide 34 text

Result を使うと、エラー分岐も網羅できる 失敗が「ビジネスロジックの値」になる。 $result = $this->orderService->placeOrder($input); if ($result->isErr()) { return match ($result->unwrapErr()) { OrderError::UserNotFound => /* ... */, OrderError::OutOfStock => /* ... */, }; // ケース漏れは PHPStan が検出 } $order = $result->unwrap(); PHP 34/41

Slide 35

Slide 35 text

PHP での Result 型実装の参考資料 https://speakerdeck.com/higaki_program/phpderesultxing-wo-narubekushi-iyasuixing-deshi-zhuang-suru https://speakerdeck.com/kajitack/exception-or-result-type 35/41

Slide 36

Slide 36 text

PHP の ADT 推進の動き enum はその一歩。Result 型や直和型がいつかサポートされるかも https://wiki.php.net/rfc/adts 36/41

Slide 37

Slide 37 text

PHP編のまとめ 想定できる失敗は「例外」ではなく、Result で値として返す データ付きの直和は Sealed クラスで作り、静的解析で閉じて絞り込む エラー分岐は match で網羅。ケース漏れは静的解析が検出する ADT の RFC が進行中。PHP の型アップデートに注目 今回紹介した Result 型以外のモデリングにも活かせるかも (今は Factory パターンで Immutable な Entity 生成をやっている) 37/41

Slide 38

Slide 38 text

型を組み立てる 型を組み立てるという観点では、代数的データ型以外にも型に 注目していくと面白いかも ジェネリクス:集合を変数にする型。PHP 8.6 型理論:述語や値で、さらに集合を絞り込む世界 38/41

Slide 39

Slide 39 text

関数型まつり 型=集合という考え方は関数型プログラミングのものなので、興味を持った方はぜひ! チケット発売中! https://2026.fp-matsuri.org/ 39/41

Slide 40

Slide 40 text

代数的データ型って何が嬉しいの? 型を「集合」として捉えると設計の武器になる 厳密な型は補完・静的解析・AI が読み取ってくれる 代数的データ型 =「直積」と「直和」で組み立てる型 不正な状態を「存在させない」型定義ができる 特に直和型はケースごとに違う構造を持てるので強力 UI の状態管理もドメインルールも同じ考え方で守れる 40/41

Slide 41

Slide 41 text

No content