【入門編】Type Aliasで定義する「状態遷移マシン」の型安全な実装パターン – TypeScript コア・型システムの基礎解析バイブル

やあ。TypeScriptの深淵へようこそ。
日々コードを書いていると、「このステート(状態)のときに、このメソッドを呼んでほしくないのに…」と頭を抱えることはないかな?

例えば、注文システムで「発送済み」の状態なのに「キャンセル」ができてしまうバグ。これ、実行時にエラーを投げて防ぐのは簡単だけど、「そもそもコンパイルが通らないようにする」のが、TypeScriptを掌握するエンジニアの矜持だ。

今日は、TypeScriptの「判別可能な共用体(Discriminated Unions)」を使って、不正な状態遷移を物理的に不可能にする、鉄壁のステートマシン設計を伝授するよ。

—

1. なぜ「型」で状態を縛るのか?

多くの人がやりがちな失敗は、すべてをひとつのオブジェクトに詰め込むことだ。

// 悪い例:全部入りで、何が正解か分からない
interface Order {
status: ‘pending’ | ‘shipped’ | ‘cancelled’;
trackingNumber?: string; // 発送済みなら必須だけど、他は不要
reason?: string; // キャンセル時のみ必要
}

これだと、`status`が`pending`なのに`trackingNumber`が入っていたり、逆に`shipped`なのに`trackingNumber`が空だったりする「ありえない状態」を許容してしまう。これがバグの温床なんだ。

—

2. 判別可能な共用体(Discriminated Unions)の極意

TypeScriptの型システムには、「タグ」を使って型を絞り込む強力な機能がある。これを使えば、状態ごとに「持てるデータ」を厳格に管理できる。

以下のコードを見てほしい。

// 各状態を独立した型として定義する
type OrderState =
| { type: ‘PENDING’ }
| { type: ‘SHIPPED’; trackingNumber: string } // 発送済みなら必ず追跡番号がある
| { type: ‘CANCELLED’; reason: string }; // キャンセルなら理由が必須

// これが状態そのものの型になる
type Order = {
id: string;
state: OrderState;
};

見ての通り、`type`という共通のプロパティ(判別子)を持たせているのがポイントだね。これがコンパイラにとっての「標識」になる。

—

3. コンパイラを味方につける「遷移関数」の実装

次に、状態を遷移させる関数を書こう。ここで「不正な遷移」をコンパイルエラーにする魔法をかけるんだ。

function shipOrder(order: Order): Order {
// 状態がPENDINGの時だけ発送できる(それ以外は型エラーになる)
if (order.state.type !== ‘PENDING’) {
throw new Error(‘発送できるのは注文待ちの状態だけです!’);
}

return {
…order,
state: { type: ‘SHIPPED’, trackingNumber: ‘TK-12345’ }
};
}

ここがポイント!

もし、`CANCELLED`状態の注文に対して誤ったデータを渡そうとすると、TypeScriptのコンパイラが即座に反応する。

const myOrder: Order = { id: ‘1’, state: { type: ‘CANCELLED’, reason: ‘在庫切れ’ } };

// コンパイルエラー!
// shipOrder(myOrder);
// 「’CANCELLED’ は ‘PENDING’ ではないので、この処理は許可されません」と怒ってくれるはずだ。

—

4. 陥りやすい罠:網羅性チェック(Exhaustiveness Check)

状態が増えてきたとき、`switch`文で書き忘れることはよくあるよね。これを防ぐために、`never`型を活用しよう。

function getStatusMessage(state: OrderState): string {
switch (state.type) {
case ‘PENDING’: return ‘準備中だよ’;
case ‘SHIPPED’: return `発送済み!追跡番号: ${state.trackingNumber}`;
case ‘CANCELLED’: return `キャンセル理由: ${state.reason}`;
default:
// ここに到達したということは、状態の定義漏れがあるということ!
const _exhaustiveCheck: never = state;
return _exhaustiveCheck;
}
}

この`default`節にある`never`への代入は、「すべてのケースを網羅しないとコンパイルが通らない」という強制力を働かせる。新しい状態を追加した瞬間に、どこを修正すべきかコンパイラが教えてくれるんだ。

—

まとめ:TypeScriptは「思考の補助輪」ではない

多くの初学者は、TypeScriptを「入力を補助してくれるツール」だと思っているかもしれない。でも、真のアーキテクトはこう考える。

「TypeScriptは、不正なコードが生まれる余地を消し去るための、論理的な証明装置である」

判別可能な共用体をマスターすれば、複雑なフロントエンドのステート管理も、Node.jsの非同期処理の遷移も、恐れることはなくなる。

まずは、あなたのプロジェクトにある「何でも入るオブジェクト」を、この「状態ごとの型」に分解することから始めてみてほしい。コードが驚くほど健やかになるはずだよ。

さあ、型安全なコードを書く旅を楽しんで!また何かあれば、いつでも聞きに来てね。

タイトルとURLをコピーしました