第15章:例外よりResult的な扱い(Command/Chain/Observerの安定化)🧯
ねらい🎯✨
throwに頼らず、失敗を「戻り値」で扱えるようになる😊- 失敗が起きても、処理の流れが壊れにくいコードになる🧱
- 「失敗もまた、一つの値である」🎁

- 「失敗の種類」を整理して、呼び出し側が迷子にならないようにする🧭
まず結論:Resultは「失敗を型で見える化」する📦👀
例外(throw)は便利だけど、慣れてくるとこんな悩みが出がち…😵💫
例外がつらくなる瞬間😵
- どこで
throwされるか 呼び出し側から見えにくい🙈 - 複数ステップ(検証→在庫→決済→通知…)で、どこで落ちたか追いづらい🌀
- 例外を握りつぶしたり、ログだけ吐いて続行して、状態が壊れる💥
Resultが気持ちいい瞬間😌✨
- 「成功 or 失敗」が 戻り値の型で必ず見える✅
- 失敗パターン(在庫切れ、入力ミス…)を 仕様として明文化できる📘
- 後の章(Command/Chain/Observer)で、**失敗を“安全に受け渡し”**できる🧷
TypeScript流のResult型:いちばん定番の形🧁
TypeScriptでは、Resultはだいたいこの形が王道だよ〜💡
// 成功: { ok: true, value }
// 失敗: { ok: false, error }
export type Result<T, E> =
| { ok: true; value: T }
| { ok: false; error: E };
export const ok = <T>(value: T): Result<T, never> => ({ ok: true, value });
export const err = <E>(error: E): Result<never, E> => ({ ok: false, error });
export const isOk = <T, E>(r: Result<T, E>): r is { ok: true; value: T } => r.ok;
export const isErr = <T, E>(r: Result<T, E>): r is { ok: false; error: E } => !r.ok;
あると超便利:match(分岐を読みやすく)🎛️
export const match = <T, E, R>(
r: Result<T, E>,
arms: { ok: (v: T) => R; err: (e: E) => R }
): R => (r.ok ? arms.ok(r.value) : arms.err(r.error));
失敗の「種類」を型で作る🧩🧯
Resultをやるときのコツは、Error だけを返すんじゃなくて、**失敗の種類(分類)**を作ることだよ〜✨
たとえば「注文確定」で起こりそうな失敗👇
export type OrderError =
| { type: "validation"; message: string; field?: string }
| { type: "out_of_stock"; productId: string }
| { type: "payment_failed"; message: string; cause?: unknown }
| { type: "unexpected"; message: string; cause?: unknown };
causeは「元の原因をぶら下げる」用途で便利だよ🧵Errorのcauseは広く使える仕組みとして整理されてるよ〜📌 (MDNウェブドキュメント)
ハンズオン:注文確定をResultで壊れにくくする☕🧾✨
お題の型(最小)🧁
export type OrderItem = { productId: string; qty: number; unitPrice: number };
export type Order = {
id: string;
items: OrderItem[];
};
export type ConfirmedOrder = Order & {
total: number;
confirmedAt: string;
};
Step 1) まずは「検証」をResultで返す🧼✅
「入力ミス」は“想定内の失敗”だから、例外よりResultが向いてるよ😊
import { Result, ok, err } from "./result";
import type { Order, OrderError } from "./types";
export function validateOrder(order: Order): Result<Order, OrderError> {
if (order.items.length === 0) {
return err({ type: "validation", message: "商品が1つも入ってないよ🥺", field: "items" });
}
for (const item of order.items) {
if (item.qty <= 0) {
return err({
type: "validation",
message: "数量は1以上にしてね🙏",
field: `items.qty(${item.productId})`,
});
}
}
return ok(order);
}
Step 2) 外部I/Oは「例外をResultに変換」して境界で止める🌐🧱
決済APIやDBみたいな外部I/Oは、例外が混ざりやすい場所! なので「境界でキャッチしてResultにする」が安全だよ🧤✨
import { Result, ok, err } from "./result";
import type { Order, OrderError } from "./types";
// 例:在庫確保(外部I/Oっぽい想定)
export async function reserveStock(order: Order): Promise<Result<Order, OrderError>> {
// ダミー:在庫切れを1つ作る
const out = order.items.find((i) => i.productId === "bean-999");
if (out) return err({ type: "out_of_stock", productId: out.productId });
return ok(order);
}
// 例:決済(外部I/Oっぽい想定)
export async function chargePayment(order: Order): Promise<Result<Order, OrderError>> {
try {
// ダミー:ランダム失敗
if (Math.random() < 0.2) {
throw new Error("gateway timeout", { cause: { retryAfterMs: 500 } });
}
return ok(order);
} catch (e: unknown) {
return err({
type: "payment_failed",
message: "決済に失敗したよ💳💦",
cause: e,
});
}
}
Error(..., { cause }) の形は「原因を持たせたい」ときに便利だよ🧵 (MDNウェブドキュメント)
Step 3) 注文確定を “順番に” つなぐ(Resultで安全に)⛓️✨
いちばん読みやすいのは「成功なら進む、失敗なら即return」💨
import { Result, ok } from "./result";
import type { Order, ConfirmedOrder, OrderError } from "./types";
import { validateOrder } from "./validate";
import { reserveStock, chargePayment } from "./io";
export async function confirmOrder(order: Order): Promise<Result<ConfirmedOrder, OrderError>> {
const v = validateOrder(order);
if (!v.ok) return v;
const s = await reserveStock(v.value);
if (!s.ok) return s;
const p = await chargePayment(s.value);
if (!p.ok) return p;
const total = p.value.items.reduce((sum, i) => sum + i.qty * i.unitPrice, 0);
return ok({
...p.value,
total,
confirmedAt: new Date().toISOString(),
});
}
Step 4) 呼び出し側は match でキレイに🎀
UIでもCommandでもObserverでも、受け取り側が迷わないのが最高〜☺️✨
import { match } from "./result";
import { confirmOrder } from "./confirm";
async function onClickConfirm() {
const result = await confirmOrder({
id: "o-1",
items: [{ productId: "latte-001", qty: 1, unitPrice: 520 }],
});
const message = match(result, {
ok: (order) => `注文OK🎉 合計 ${order.total}円だよ〜☕`,
err: (e) => {
switch (e.type) {
case "validation":
return `入力ミスかも💦 ${e.message}`;
case "out_of_stock":
return `ごめんね🙏 在庫切れ: ${e.productId}`;
case "payment_failed":
return `決済NG💳 ${e.message}`;
default:
return `想定外エラー😵💫 ${e.message}`;
}
},
});
console.log(message);
}
どこまでResultにする?“全部Result”は疲れるよ😂🧠
コツはこれ👇
- ✅ 想定内の失敗(入力ミス、在庫切れ、権限NG、外部I/O失敗)→ Result
- ⚠️ 想定外のバグ(null参照、あり得ない状態)→ 例外でもOK(直すべき)🛠️
つまり、Resultは「仕様として起きうる失敗」を扱う道具だよ📌✨
テストして安心しよ🧪💕(最小)
例は vitest っぽい書き方にしておくね(読みやすさ重視)💡
import { describe, it, expect } from "vitest";
import { validateOrder } from "./validate";
import { confirmOrder } from "./confirm";
describe("validateOrder", () => {
it("商品が空ならvalidationエラー", () => {
const r = validateOrder({ id: "o-1", items: [] });
expect(r.ok).toBe(false);
if (!r.ok) {
expect(r.error.type).toBe("validation");
}
});
});
describe("confirmOrder", () => {
it("正常ならokでtotalが入る", async () => {
const r = await confirmOrder({
id: "o-2",
items: [{ productId: "latte-001", qty: 2, unitPrice: 520 }],
});
expect(r.ok).toBe(true);
if (r.ok) {
expect(r.value.total).toBe(1040);
}
});
});
つまずきポイントあるある😵💫➡️😌(回避ワザつき)
-
エラー型が増えすぎる📦 →
typeは大分類だけにして、細かい情報はフィールドで持つ(例:message,field)🧩 -
ifが多くて読みにくい🌀 → まずは「早期return」でOK!慣れたら
andThenみたいな補助関数を追加しても良いよ✨ -
外部I/Oの例外が混ざってカオス🌪️ → 境界で
try/catch→err(...)に変換して、内側をきれいに保つ🧤 -
causeの型が怒られる😡 →causeはES2022以降で整ってるので、型的に困ったらunknownで受けて、ログ用途に回すのが安全🧵 (MDNウェブドキュメント) -
「全部Result」にして疲れる🫠 → 仕様上“起こりうる失敗”だけResultにする(境界中心でOK)🧭
ミニ課題(5〜15分)⏱️🌸
OrderErrorにtype: "store_closed"を追加してみよう🏪🌙toUserMessage(error)を作って、UI表示の文言を一箇所に集めよう🪄payment_failedのときだけcauseをログに出すようにしてみよう🧾
AIプロンプト例🤖💬✨
Result型で例外を減らしたいです。
- 成功/失敗のdiscriminated unionでResult<T,E>を提案して
- OrderErrorの分類案(validation/out_of_stock/payment_failed/unexpected)も
- confirmOrder(検証→在庫→決済→合計)をResultで実装して
- 呼び出し側のmatch例と、テストケース案も
この関数はthrowしていて呼び出し側が辛いです。
「想定内の失敗」と「想定外のバグ」を分けて、
想定内だけResultに変換するリファクタ案を段階的に出して。
OrderErrorが増えすぎて困ってます。
分類の粒度(typeの種類)を減らしつつ、
UI向けメッセージとログ向け情報(cause等)を両立する設計にして。
2026のTypeScript小ネタ(超短)🗞️✨
- npm上の安定版は TypeScript 5.9.3 が “Latest” として掲載されているよ📌 (npm)
- そして **TypeScript 6.0 は「7.0への橋渡し(bridge)」**として位置づけられていて、7.0はネイティブ化(Goベース)に向かって進捗が出てるよ🚀 (Microsoft for Developers)
(Resultの書き方自体は、5.x系でもそのまま通用するよ😊)