第18章:Eventを型にする(判別可能ユニオン)🎫✨
この章は「イベントが増えても破綻しない型の作り方」を身につける回だよ〜😊 状態(State)を型にできたら、次は イベント(Event)も型で守ると一気に強くなる!💪✨
0. 2026年1月時点の“いま”メモ🗓️🔍
- TypeScript は 5.9 系が安定ラインで、公式でも 5.9 のリリース告知が出てるよ📌 (Microsoft for Developers)
- 一方で **TypeScript 6.0(ブリッジ)→ 7.0(ネイティブ)**への大きい移行が進行中。ネイティブプレビュー(
@typescript/native-preview)や進捗も公式が出してるよ🚀 (Microsoft for Developers)
この章の内容(判別可能ユニオン)は、5.9系でも超ど真ん中に使えるから安心してOK👌✨ (TypeScript)
1. 今日のゴール🎯💖
この章を終えたら、こんなことができるようになるよ!
- ✅ Event を 判別可能ユニオンで定義できる(
typeを目印にする) (TypeScript) - ✅
switch (event.type)で 型が勝手に絞り込まれる(narrowing) (TypeScript) - ✅ 新しいイベントを追加したときに ハンドリング漏れをコンパイルで気づける(
never) (TypeScript)
2. Eventってなに?(状態機械の“入力”)📮✨
状態機械はざっくりこうだったよね👇
- State(状態):いま何をしてるか
- Event(イベント):何が起きたか(ユーザー操作 / タイマー / API結果…)
- Transition(遷移):状態がどう変わるか
Event を雑に string で扱うと、イベント名の打ち間違いで死にやすい…😇
だから イベントの種類と、イベントごとの“持ち物(payload)”を型にするよ🎁✨
3. 判別可能ユニオン(Discriminated Union)って?🔖🧸

コツはこれだけ!
- 全イベントに 共通の目印を持たせる(よくあるのは
type) typeを 文字列リテラル("SUBMIT"みたいに固定)にする- そうすると TypeScript が
switchやifで 型を絞り込める (TypeScript)
4. まずは “イベント型” を作ろう🎫✨(基本形)
フォーム送信を題材に、よくあるイベントを作るね😊
// 例: フォームで扱うフィールド
type Field = "email" | "password";
// Event(判別可能ユニオン)
export type Event =
| { type: "START_EDIT" }
| { type: "CHANGE_FIELD"; field: Field; value: string }
| { type: "SUBMIT" }
| { type: "SUBMIT_SUCCESS"; requestId: string }
| { type: "SUBMIT_FAILURE"; message: string; retryable: boolean }
| { type: "RESET" };
ポイント💡
type: "CHANGE_FIELD"みたいに 固定文字列にするのが超大事!- イベントごとに必要なデータだけ持たせる(
SUBMIT_SUCCESSはrequestIdほしいよね〜🔍)
5. switch(event.type) で “勝手に型が合う” 快感😆✨
イベントを処理する側(Reducer/Transition)でこうなる👇
case の中では event がその型に絞られるよ! (TypeScript)
import type { Event } from "./events";
function handleEvent(event: Event) {
switch (event.type) {
case "START_EDIT":
// event は { type: "START_EDIT" }
return "start editing!";
case "CHANGE_FIELD":
// event は { type: "CHANGE_FIELD"; field: Field; value: string }
return `changed ${event.field} = ${event.value}`;
case "SUBMIT_SUCCESS":
// event は { type: "SUBMIT_SUCCESS"; requestId: string }
return `ok! requestId=${event.requestId}`;
case "SUBMIT_FAILURE":
return event.retryable ? "retry?" : "give up...";
case "SUBMIT":
case "RESET":
return "simple event";
default:
// 後で “漏れ検出” を強くするよ!
return "unknown";
}
}
6. ハンドリング漏れを “コンパイルで止める” 🚫✅(never のやつ)
イベントが増えたのに switch を直し忘れる…あるある😇
ここで never を使うと、漏れた瞬間に型エラーにできるよ! (TypeScript)
function assertNever(x: never): never {
throw new Error("Unexpected event: " + JSON.stringify(x));
}
function handleEventStrict(event: Event) {
switch (event.type) {
case "START_EDIT":
return "start editing!";
case "CHANGE_FIELD":
return `changed ${event.field}`;
case "SUBMIT":
return "submit";
case "SUBMIT_SUCCESS":
return event.requestId;
case "SUBMIT_FAILURE":
return event.message;
case "RESET":
return "reset";
default:
// Event に新しい type を足すと、ここが型エラーになる✨
return assertNever(event);
}
}
これで「直し忘れ」が ビルド時に即バレするの、めちゃ強い😳💕
7. よくある沼ポイント🫠(ここ超大事)
❌ type: string にしちゃう
// ダメ例: 絞り込みが効かない😇
type BadEvent = { type: string; payload?: unknown };
これだと switch (event.type) しても「結局 string でしょ?」ってなって、型の恩恵が激減…💦
❌ payload を全部 any にまとめる
type BadEvent =
| { type: "SUBMIT_SUCCESS"; data: any }
| { type: "SUBMIT_FAILURE"; data: any };
“便利そう”に見えて、実はバグが増えるやつ〜😇 イベントごとに必要な形をちゃんと持たせよ🎁✨
8. “イベント作成関数” を用意すると、さらに事故が減る🧯✨
毎回 { type: "...", ... } を手打ちすると、地味にミスるのよ…🥺
だから **イベントを作る関数(creator)**を用意しよ!
import type { Event } from "./events";
export const E = {
startEdit(): Event {
return { type: "START_EDIT" };
},
changeField(field: "email" | "password", value: string): Event {
return { type: "CHANGE_FIELD", field, value };
},
submit(): Event {
return { type: "SUBMIT" };
},
submitSuccess(requestId: string): Event {
return { type: "SUBMIT_SUCCESS", requestId };
},
submitFailure(message: string, retryable: boolean): Event {
return { type: "SUBMIT_FAILURE", message, retryable };
},
reset(): Event {
return { type: "RESET" };
},
};
VS Code の補完がめちゃ気持ちいいやつ…😆💞
9. satisfies を使うと “形のチェック” が上手くできる🧩✨(おすすめ)
events の一覧とか「この形で揃っててほしい!」ってとき、satisfies が便利だよ🪄
(値の推論を潰さずに “満たしてるか” をチェックできる) (TypeScript)
例:イベントの表示名辞書を、漏れなく揃える👇
import type { Event } from "./events";
type EventType = Event["type"];
export const eventLabel = {
START_EDIT: "編集開始",
CHANGE_FIELD: "入力変更",
SUBMIT: "送信",
SUBMIT_SUCCESS: "送信成功",
SUBMIT_FAILURE: "送信失敗",
RESET: "リセット",
} satisfies Record<EventType, string>;
Event を増やしたのに辞書を足し忘れたら、ここで気づける!👏✨
10. ミニ演習🎓🌸(手を動かすと一気に覚える!)
演習A:イベントを1つ追加しよう➕🎫
CANCEL を追加してみてね(payloadなし)
Eventに{ type: "CANCEL" }を足すhandleEventStrictが型エラーになるのを確認case "CANCEL":を足して直す
演習B:payload設計してみよう🎁
VALIDATION_FAILED を追加!
errors: Array<{ field: Field; message: string }>を持たせてみよう✨
11. AI活用プロンプト集🤖💖(そのままコピペOK)
- 「フォーム送信の状態機械で、必要になりそうなイベントと payload 候補を列挙して。過不足も指摘して」
- 「この Event ユニオン、命名を統一したい。命名ルール案とリネーム案ちょうだい」
- 「イベントごとの payload が重すぎないかレビューして。Context に寄せるべきものも教えて」
- 「
switch(event.type)の漏れをneverで検出する形にリファクタして」 - 「
eventLabel辞書をsatisfiesで漏れ検出できる形にして」 (TypeScript)
12. この章のまとめ🧁✨(チェックリスト)
- ✅ Event は
{ type: "..." }を共通目印にした 判別可能ユニオンで作る (TypeScript) - ✅
switch(event.type)で型が絞られて、payload が安全に扱える (TypeScript) - ✅
assertNeverで イベント追加時の漏れをコンパイルで検出できる (TypeScript) - ✅
satisfiesを使うと「辞書や設定の漏れ検出」が気持ちよくできる (TypeScript)
次の第19章では、State だけじゃ表せない“データ”を Context に入れて、状態機械がさらに現実のアプリっぽくなるよ〜🧠💖