はじめに
前回
ビジネスロジックの核心には「状態遷移」がある。注文処理、ワークフロー、タスク管理など、あらゆるドメインは状態と遷移の集合で表現できる。本稿では TypeScript の型システムを用いて 不正遷移をコンパイルエラーで防止 する設計手法を解説する。
1. 状態遷移を型で表す意義
- 仕様ドキュメントと実装が乖離しない。
- ランタイムチェック不要のロジック保証。
- IDE 補完で遷移可能なオペレーションが自明になる。
2. ケーススタディ:EC 注文フロー
状態 | 遷移イベント |
---|---|
Placed |
pay / cancel
|
Paid |
ship / refund
|
Shipped |
deliver |
Delivered |
— |
Cancelled |
— |
Refunded |
— |
3. Discriminated Union で表現
type OrderState =
| { status: "Placed" }
| { status: "Paid"; paidAt: Date }
| { status: "Shipped"; tracking: string }
| { status: "Delivered"; deliveredAt: Date }
| { status: "Cancelled"; reason: string }
| { status: "Refunded"; refundedAt: Date };
-
status
が識別子(discriminator)となり、条件分岐で型が絞り込まれる。
4. 遷移関数を型安全に実装
function pay(order: Extract<OrderState, { status: "Placed" }>): Extract<OrderState, { status: "Paid" }> {
return { status: "Paid", paidAt: new Date() };
}
-
Extract<T,U>
で許可された状態のみ受け取り、遷移後の状態を返す。 - 誤って
Paid
注文に対してpay()
を呼ぶと コンパイルエラー。
汎用遷移ヘルパー
type Transition<S, T> = (state: S) => T;
const ship: Transition<
Extract<OrderState, { status: "Paid" }>,
Extract<OrderState, { status: "Shipped" }>
> = (o) => ({ status: "Shipped", tracking: "TRACK123" });
5. Finite State Machine (FSM) マップ化
type TransitionMap = {
Placed: "Paid" | "Cancelled";
Paid: "Shipped" | "Refunded";
Shipped: "Delivered";
Delivered: never;
Cancelled: never;
Refunded: never;
};
type Next<S extends keyof TransitionMap> = TransitionMap[S];
-
Next<"Placed">
は"Paid" | "Cancelled"
。 - ジェネリクスで動的に「遷移先のみ許可」する関数作成が可能。
6. 網羅性チェックに never
を利用
function assertNever(x: never): never {
throw new Error("Unexpected state: " + x);
}
function handle(order: OrderState) {
switch (order.status) {
case "Delivered":
console.log("完了");
break;
// ... 他ケース省略
default:
assertNever(order); // 新しい状態追加時にコンパイルエラー
}
}
7. Zod と連携したランタイム保証
import { z } from "zod";
const PlacedSchema = z.object({ status: z.literal("Placed") });
const PaidSchema = PlacedSchema.extend({ status: z.literal("Paid"), paidAt: z.date() });
// ... 省略
type Placed = z.infer<typeof PlacedSchema>;
- API 受信時に Zod でスキーマ検証 → 上記状態型に合流。
8. 落とし穴と最適化
落とし穴 | 対策 |
---|---|
状態追加時にロジック漏れ |
never 網羅性チェックで強制エラー |
状態フィールドが肥大化 | 状態ごとに intersection で共通型を分離 |
複数 FSM が互いに依存 | コンポジションより ステートマシンライブラリ 併用を検討 |
まとめ
- 状態遷移を Discriminated Union で定義し、遷移関数で 型安全なガード を掛ける。
-
Extract
/ ジェネリクス /never
網羅チェックで 不正遷移をコンパイル時に排除 できる。 - Zod と組み合わせることで、外部データ流入部も含め エンドツーエンドの型安全 が完成する。
次回は ランタイムパフォーマンスと型安全性の両立 をテーマに、高速 API 通信と型保証のベストプラクティスを掘り下げる。