第04章:TypeScript基礎おさらい(イベント用)🔷🧩
4.0 今どきのTypeScript事情(超ミニ)🗞️✨
2026年1月時点だと、TypeScriptは 5.9系 が安定版として提供されています(例:5.9.3)。(GitHub) (この章で扱う「リテラル型・readonly・ユニオン型・型ガード」は、5.xでずっと“ど真ん中”の基礎だよ〜😊)
4.1 この章のゴール🎯💖
この章が終わると、こんなことができるようになります👇✨
- イベントを 「型」で安全に扱う(間違ったイベント名をコンパイルで止める🚫)
- イベントを readonly にして「勝手に書き換え」事故を減らす🔒
- イベントを ユニオン型 でまとめて、
switchで分岐できる⚖️ - 外から来る
unknownなデータを 型ガード で安全にイベント化する🛡️
4.2 まずイベント名を「リテラル型」で固定しよう🏷️✨
イベント名は「文字列」だけど、ただのstringにしちゃうと危険⚠️
"OrderPaid"のつもりで "OrderPiad" みたいなタイポでも、stringだと通っちゃう🥲
ここは 文字列リテラル型 を使って「許可された名前だけ」にします💪✨ (TypeScript)
✅ いちばん簡単:ユニオン型でイベント名を列挙🧾
export type EventType =
| "OrderPlaced"
| "OrderPaid";
これで EventType は 2つの文字列しか許さない 型になるよ🙌
4.3 as constで「配列からEventTypeを作る」📦➡️🏷️
イベントが増えると、ユニオンを手で書くのがだるい…😵💫 そこで「配列を1個作って、そこから型を作る」テクが便利✨
as const(constアサーション)を使うと、配列の中身が リテラル型として固定 されます。(TypeScript)
export const EVENT_TYPES = ["OrderPlaced", "OrderPaid"] as const;
export type EventType = typeof EVENT_TYPES[number];
// ^ "OrderPlaced" | "OrderPaid" になる🎉
💡 ここ大事(初心者つまずきポイント)⚠️
as constがないとこうなる👇(イベント名がただのstringに“広がる”ことがある)
- 「固定したいのに、固定されない」=型のメリットが減る😢
4.4 イベントは「勝手に変わらない」でいてほしい:readonly🔒🧊
イベントは「起きた事実」なので、あとから内容が書き換わるのって怖いよね😱
TypeScriptでは readonly をつけると、代入(書き換え)を型チェックで禁止できます。(TypeScript)
✅ 共通フォーマット(基本形)🧾✨
export type DomainEvent<TType extends string, TPayload> = {
readonly eventId: string;
readonly occurredAt: string; // まずはISO文字列でOK🕰️
readonly aggregateId: string;
readonly type: TType;
readonly payload: TPayload;
};
readonlyは「実行時に凍結する」わけじゃなくて、型チェック上で禁止してくれる仕組みだよ🧠✨ (TypeScript)
4.5 payloadはイベントごとに違う:ユニオン型でまとめる🔀🎁

イベントごとにpayloadの形が違うのが普通だよね😊
そこで、イベントを ユニオン型 でまとめて、typeで判別できるようにします✨(これが「判別可能ユニオン」的な考え方🌈)(TypeScript)
✅ イベント型を2つ作る(例:注文作成・支払い完了)🛒💳
export type OrderPlaced = DomainEvent<
"OrderPlaced",
{
readonly orderId: string;
readonly customerId: string;
readonly total: number;
}
>;
export type OrderPaid = DomainEvent<
"OrderPaid",
{
readonly orderId: string;
readonly paidAt: string;
readonly method: "card" | "bank";
}
>;
export type AppEvent = OrderPlaced | OrderPaid;
✅ switchで安全に分岐できる🎛️✨
TypeScriptはtypeを見て、payloadの型を絞り込み(narrowing)してくれます💡 (TypeScript)
export function handleEvent(e: AppEvent) {
switch (e.type) {
case "OrderPlaced":
console.log("total =", e.payload.total);
break;
case "OrderPaid":
console.log("method =", e.payload.method);
break;
}
}
4.6 型ガード:unknownから安全にイベントへ🛡️👀
外部から来るデータ(JSONなど)は、まずunknownとして受けるのが安全✨
そして「本当にイベントっぽい形?」をチェックしてから AppEvent にします✅
型ガードは「条件で型を絞る」ための仕組みだよ〜!(TypeScript)
4.6.1 型ガード3パターン(最低限これでOK)🧰✨
① typeof(プリミティブ判定)🔍
function isString(v: unknown): v is string {
return typeof v === "string";
}
typeofによる絞り込みはTypeScriptの基本テクだよ🧠(TypeScript)
② in(プロパティがあるか)🏷️
function hasProp(obj: object, key: string): obj is Record<string, unknown> {
return key in obj;
}
inで「そのキーがある」ことをチェックして絞り込めるよ✨(TypeScript)
③ 「自作の型ガード関数」(v is AppEvent)🧪
これが一番“イベントっぽい”やつ😆✨
type AnyObj = Record<string, unknown>;
function isRecord(v: unknown): v is AnyObj {
return typeof v === "object" && v !== null;
}
function hasStringField(obj: AnyObj, key: string): boolean {
return typeof obj[key] === "string";
}
export function isAppEvent(v: unknown): v is AppEvent {
if (!isRecord(v)) return false;
// 必須フィールドが文字列かチェック🧾
if (!hasStringField(v, "eventId")) return false;
if (!hasStringField(v, "occurredAt")) return false;
if (!hasStringField(v, "aggregateId")) return false;
if (!hasStringField(v, "type")) return false;
// payloadがあるか(中身はイベントごとにさらに検証してもOK)🎒
if (!("payload" in v)) return false;
// typeが許可されたものか🏷️
return v.type === "OrderPlaced" || v.type === "OrderPaid";
}
「ユニオンのどれか?」の判定は、
typeがリテラルで固定されてると超やりやすいよ😊(TypeScript)
4.7 “広がっちゃう”事故(widening)と対策⚠️🧯
TypeScriptはたまに、リテラルを stringに広げちゃうことがあるよ〜🥲
この章で超重要なのは、「イベント名は広げない」こと!
ありがちな事故😵💫
const e = {
type: "OrderPlaced",
payload: { orderId: "o1", customerId: "c1", total: 1000 },
};
// e.type が "OrderPlaced" じゃなくて string っぽく扱われて困ることがある…💥
対策①:as constで固定する🔒
const e = {
type: "OrderPlaced",
payload: { orderId: "o1", customerId: "c1", total: 1000 },
} as const;
as constは「できるだけ具体的な型(リテラル型)で推論」してくれる仕組みだよ✨(TypeScript)
対策②:satisfiesで“満たしてるか”だけ確認する✅(便利!)
:で型注釈しちゃうと、推論が弱くなることがあるけど、satisfiesは「チェックしつつ推論を保つ」方向💡(TypeScript)
type EventTypeToPayload = {
OrderPlaced: { orderId: string; customerId: string; total: number };
OrderPaid: { orderId: string; paidAt: string; method: "card" | "bank" };
};
const samplePayloads = {
OrderPlaced: { orderId: "o1", customerId: "c1", total: 1000 },
OrderPaid: { orderId: "o1", paidAt: "2026-01-27T00:00:00Z", method: "card" },
} satisfies EventTypeToPayload;
4.8 ミニ演習(手を動かすやつ)📝💪✨
演習1:イベント名ユニオンを作る🏷️
次を作ってみよう👇
type EventType = "OrderPlaced" | "OrderPaid"✅(ロードマップのやつ!)
演習2:共通イベント型を作る🧾
DomainEvent<TType, TPayload>を作るeventId / occurredAt / aggregateId / type / payloadをreadonlyにする🔒
演習3:switchで分岐してpayloadに触る🎛️
AppEvent = OrderPlaced | OrderPaidを作るswitch (e.type)でtotalとmethodを取り出す💳
演習4:型ガードを3種類書く🛡️
typeofを使うやつinを使うやつv is AppEventを返すやつ(自作型ガード)
4.9 AI活用(そのままコピペOK)🤖🪄💬
✨ イベント名・payload設計のたたき台
- 「
OrderPlacedとOrderPaidのpayloadを、最小限で2案ずつ提案して。個人情報は入れないで。使う側が困らない形にして。」
✨ 型ガードを3パターン生成してもらう(ロードマップ準拠)
- 「TypeScriptで
unknownをAppEventにする型ガードを3パターン。typeof/in/ 自作型ガード(type predicate)でお願い。初心者向けコメント多めで。」
✨ widening事故チェック
- 「このコードで
typeがstringに広がる可能性ある?あるなら修正案を2つ(as constとsatisfies)で出して。」
4.10 まとめ(この章の“芯”)🌟
- イベント名は リテラル型 で固定する🏷️ (TypeScript)
- イベントは readonly で「書き換え事故」を減らす🔒 (TypeScript)
- イベントは ユニオン型+
typeで分岐すると強い🎛️ (TypeScript) - 外から来るものは 型ガード で安全に扱う🛡️ (TypeScript)
次章からは「混ぜない(SoC)」「境界」みたいな“設計の入口”に入っていくよ〜🧱✨