Upgrade to Pro — share decks privately, control downloads, hide ads and more …

TypeScript の型で副作用の実行順序を制御する

Sponsored · Your Podcast. Everywhere. Effortlessly. Share. Educate. Inspire. Entertain. You do you. We'll handle the rest.

TypeScript の型で副作用の実行順序を制御する

TSKaigi 2026 の発表資料です
https://2026.tskaigi.org/talks/35

Example コードはこちら
https://github.com/yanaemon/type-safe-effect-order-example

バリデーション前にデータを保存する、ログを取らずに重要な処理を実行する、認証前に API を呼ぶ。
副作用を持つ処理では実行順序のミスがデータ破損やセキュリティ問題を引き起こします。
テストを書く前、コードを実行する前に、エディタ上で即座に気づきたい。
これをコンパイル時に防げないか。
本トークでは、TypeScript の型システムで副作用の実行順序をどこまで制御できるのかを探ります。
また、純粋関数でもデータの形状を保証するために実行順序の制約が有効であることを示します。

Avatar for yanaemon

yanaemon

May 22, 2026

More Decks by yanaemon

Other Decks in Technology

Transcript

  1. 自己紹介 About me 白栁 広樹 / Hiroki Shirayanagi 株式会社ミツモア /

    MeetsMore inc. プロダクト本部長 2013 年 : ヤフー 2018 年 : ミツモア Full-Stack TypeScript でプロダクト作っています! @yanaemon @yanaemon169
  2. 今日のスコープ Scope 今日話すこと / 話さないこと 今日話すこと 副作用を持つ関数の実行順序を “型”で 制御する方法 関連ライブラリとの簡単な比較

    今日話さないこと 副作用の合成・モデル化 (例. Effect-TS) 関連ライブラリの詳細な内容 (例. Effect-TS / XState / etc…)
  3. TypeScript は 副作用 をどこまで表現できるか 問題提起 Problem 副作用 = 関数が入出力以外で、外部の状態を取得したり変更したりする処理 (例.

    DB 書込 / 通知 / I/O / console.log 等) TS の型で表現できる / できない ◎ 引数 / 返り値の形 // ◎ 引数 / 返り値 — 副作用のない純粋関数なら型で十分 function add(a: number, b: number): number { ... } // ◯ async / Promise — 非同期は伝わる async function fetchUser(id: string): Promise<User>; // ⚠ Promise<void> — 結果なし、何が起きるか(副作用)は不明 // 「何の副作用が起きるか (予定)」「何が起きたか (事実)」 // → どちらも型に出ない async function saveUser(u: User): Promise<void>; ◯ 非同期かどうか / 失敗の可能性 ⚠ 副作用が起こる予定・起きた事実
  4. 業務処理は 副作用の連鎖 — 順序ミスしてもコンパイルを通る 問題提起 Problem validate → save →

    notify の順番を間違えると runtime バグ. 気付くのは実行時エラーで // 📝 よくある副作用を伴う処理(ユーザー作成) type UserData = { name: string, age: number } const userData = { name: 'ミツモア', age: 30 } class UserService { validate(input: UserData): boolean { ... } // Pure? async save(input: UserData): Promise<void> { ... } // 副作用 (DB書 込) async notify(input: UserData): Promise<void> { ... } // 副作用 (通 知) } const userService = new UserService() // ❌ save を忘れて notify を実行 userService.notify(userData) // ❌ validate を忘れて save を実行 userService.save(userData) 連鎖する DB → 通知 → 外部 API → 集計更新 失敗しうる 各ステップでネットワーク / DB エラー 順序が命 間違えると不正データ / 存在しない ユーザーへの通知 など 業務での副作用の特徴 副作用の「順序」を 型で守りたい!
  5. 副作用・状態遷移ライブラリ Libraries 副作用や状態遷移を扱う ライブラリ 副作用モデル系 (例. Effect-TS) 副作用を Effect<A, E,

    R> で型化して合成するアプローチ 副作用 / エラー型 / 依存性 / リソース を型で扱う flatMap で順序を 定義 できる 副作用の表現や実行順の定義はできるが、 型で縛るには追加実装が必要 状態機械系 (例. XState) 状態遷移を runtime actor で管理するアプローチ 状態 / 並列 / 階層 / 可視化 を扱える runtime actor が不正遷移を 無視 する ★本トークのゴール 「型だけで」業務順序を強制する方法はないか 順序強制は基本的には runtime 型で縛るには追加実装が必要 2
  6. 解法 ① Phantom Type 基本方針: 値の型に「処理済み」ラベルを貼る (Phantom Type) ライブラリ無し・TS 標準の型だけで考えてみる

    順序ミスはコンパイル時に止まる ランタイムコスト 0 / ライブラリ不要 ポイント 値の型に「validate を通った」ラベルを 貼って引数の型を限定 → 順序ミスをコンパイル時に止める // 💡 Point 1. Phantom Type をデータに付与 . コンパイル時には消える type ValidatedUserData = UserData & { readonly _state: "validated" }; type SavedUserData = ValidatedUserData & { readonly _state: "saved" }; type NotifiedUserData = ValidatedUserData & { readonly _state: "notified" }; class UserService { validate(input: UserData): ValidatedUserData { ... } // 💡 Point 2. 引数と返り値を state 付きの型にすることで順序を強制 async save(input: ValidatedUserData): Promise<SavedUserData> { ... } async notify(input: SavedUserData): Promise<NotifiedUserData> { ... } } const userService = new UserService() // 🚨 Argument of type '...' is not assignable to ... 'SavedUserData'. userService.notify(userData) // 🚨 Argument of type '{ ... }' is not assignable to ... 'ValidatedUserData'. userService.save(userData) // 無事コンパイルエラーに 🎉 解決 ⁉
  7. ちょっと待って Step Back ラベル付け ≠ 状態管理 やったこと 値の型に状態ラベルを乗せた 状態が 引数・返り値の型

    に乗っている → Class instance 自体は無状態、値が状態を持ち歩く userData as ValidatedUserData を出来てしまう...😱 限界 本質 「進行中のプロセス」「副作用が走ったかどうか」は状態 → 状態 × 振る舞いを「箱」に集めるのが本来形 → クラスの型パラメータに状態を持たせれば?
  8. 解法 ② (本命) Type-State Pattern クラスの型パラメータに状態を持たせる ポイント instance = 進行中のプロセス

    状態 + 振る舞いを 1 つの箱に集約 状態 = 世界へのコミット "saved" = DB に行が存在する事実 Promise reject = 遷移しない save 失敗 → saved instance なし → 通知は型で呼べない class UserService<State = "draft"> { // 💡 Point 1. State を型として保持する phantom field // declare で、コンパイル後に型は消える! declare private readonly _state: State; private constructor(private readonly data: UserData) {} validate(this: UserService<"draft">): UserService<"validated"> | null { ... return new UserService<"validated">(this.data) } // 💡 Point 2. 引数と返り値を state 付きの型にすることで順序を強制 async save(this: UserService<"validated">): Promise<UserService<"saved">> { ... return new UserService<"saved">(this.data) } ... }
  9. Type-State Pattern Demo 動かしてみる // ✅ 正しい順序 (await で次の状態を unwrap)

    const userService = new UserService(userData) const validated = userService.validate() if (validated) { const saved = await validated.save() // 副作用 (DB 書込) await saved.notify() // 副作用 (通知送信 ) } // ❌ 型エラー // 🚨 ... 'UserService<"draft">' is not assignable to ... 'UserService<"validated">'. await userService.save() // 🚨 ... 'UserService<"validated">' is not assignable to ... 'UserService<"saved">'. await userService.validate()!.notify() Type-State の威力 IDE 補完が絞られる 「今呼べるメソッドだけ」を提示 型エラーで止まる validate を飛ばして save → コンパイル不可 状態の型を構造化すれば表現力 UP { _state: “validated” | “saved” | …} → { validated: boolean; saved: boolean; … } 複雑度が上がると型が大量になり 読みにくくなる可能性 副作用が大きい重要な部分に限定すると良い 例. 金銭・セキュリティ関連、外部境界など
  10. なぜ効くか Why It Works 副作用の事実 を型で記録する 副作用の「予定」に加えて、Type-State は副作用の「事実」を記録 // 🕐

    副作用の「予定」を型で記録 save(d: Data): Effect<void, DBError, Database>> { ... } // 「DB に書く予定」「失敗しうる」 ↑ ↑ // 「Database 依存が必要」 ↑ // ✅ Type-State: 副作用の「事実」を型で記録 async function save(this: UserService<"validated">): Promise<UserService<"saved">> { await db.users.insert(this.data); return new UserService<"saved">(this.data); } // ↑ 「DB に書いた事実」を型タグで記録 // 次の notify は <"saved"> を要求できる 3 つの本質 instance = 進行中のプロセス 状態 + 振る舞いを 1 つの箱に集約 状態 = 世界へのコミット "saved" = DB に行が存在する事実 失敗時は次の状態が作られない 副作用 reject → saved instance 不在 → 後続メソッドは型レベルで呼べない 補完関係 — 予定 + 事実 (Type-State) で完全に 8 ©ミツモア
  11. 実行モデル Eager vs Lazy 実行のタイミング — 即時実行 (eager) vs 遅延実行

    (lazy) 今までは eager での実行例. lazy パターンで、副作用を持つ処理を「値」として組み立てると、全体を把握できる // Eager: 呼んだ瞬間に副作用が走る const validated = service.validate(); const saved = await validated.save(); // ← この場で DB 書込 await saved.notify(); // ← この場で通知送信 // Lazy: pipeline を 型に積み上げる const program = Program.start<UserService<"draft">>() .add({ type: "validate", fn: (u) => u.validate()! }) .add({ type: "save", fn: (u) => u.save() }) .add({ type: "notify", fn: (u) => u.notify() }); // ↑ hover で 全 step の I/O 型 (pipeline 全体) が見える // Program<[ // US = UserService // ["validate", US<"draft">, US<"validated">], // ["save", US<"validated">, US<"saved">], // ["notify", US<"saved">, US<"notified">], // ]> await program.run(initial); // ← ここで初めて実行 Lazy + 型積み上げ pipeline 全体が型に出る hover で各 step の I/O 観測 実行前に検査できる 構造から validation / dry-run ドメインと分離可 pipeline 構築自体は汎用的な処理 再利用・合成 pipeline を値として渡せる Type-State は遅延実行も可(構築時に型エラー). 本格運用なら ライブラリに任せる のが現実的
  12. 拡張 Extension 拡張方法は様々 — 場面に応じて使い分け Type-State Pattern は 1 つの基本形。状態を型で扱うパターンは多数

    1 State 型を豊かに { _state: “validated” | “saved” | … } / { validated: boolean; saved: boolean; … } union で積み上げ / 構造化 / メタデータ保持 — ドメインに応じて拡張 2 Type Predicate / Assertion isValidated(): this is UserService<"validated"> / assertValidated(): asserts this is ... runtime check に基づいて型を絞り込む 3 Narrowing (別 Interface や Omit などで制限) type Draft = Omit<UserService, "save" | "notify"> 1 interface + Omit で API を絞る / 補完にも出ない 4 Dispatcher Pattern dispatch(state, { type: "VALIDATE" }) 判別 union + dispatch / JSON 化可 / Redux 系 9 他にも色々...
  13. 位置づけ Where It Fits 観点 Phantom Type Type-State 副作用モデル系 (ex.

    Effect-TS) 状態管理系 (ex. XState) 状態の在り処 値の型 クラスの型 値の型 (合成入出力) runtime インスタンス 順序の強制 型 型 △ 単独不可 (要 Phantom/Type-State) runtime が基本 +α 機能 — — エラー型 / 並行 / cancel 並列 / 階層 / 可視化 ライブラリ / runtime 不要 不要 必要 (重) ※ Effect-TS の場合 FP 前提 必要 (中) 適性ドメイン 値ラベル 簡易な順序 副作用モデル全体 複雑遷移 / UI 状態 → 順序強制ができる 3 つの中で、Type-State Pattern は依存ゼロ — 副作用モデル系は組み合わせれば補完 既存ライブラリとの比較
  14. 設計判断 Checklist どれを選ぶか — 上から順に当てはまるものを選ぶ 1 複雑な状態遷移 (parallel / hierarchy

    / history) が必要 → 状態機械系 (例: XState) 2 副作用モデル全体 (エラー型 / 並行 / cancel) を型で扱いたい ※ Effect-TS の場合 FP 前提 → 副作用モデル系 (例: Effect-TS) 3 値中心の軽量ラベル (識別子の区別など) → Phantom Type ★ 上記以外 (= 副作用順序を 型だけで 守りたい) → Type-State Pattern Type-State Pattern + 副作用モデル系を組み合わせるとさらに強固に Type-State で順序、Effect でエラー型・並行・cancel
  15. まとめ Conclusion 値のラベル から、 副作用の実行順 を縛る型 へ 1 Phanttom Type

    / Type-State Pattern で副作用の「事実」を型で記録できる 2 予定 + 事実 は両方を型で表すことで、副作用全体を型で守れる 3 既存のライブラリと組み合わせることでより強力に 例. 実行順の制御 & 強制 + 並行・キャンセル・エラーハンドリング・etc…