第30章:総合演習(AI込み)ミニプロジェクト🎓🤝🤖💖
題材:推し活グッズ管理(予算💰・在庫📦・外部支払いAPI💳)
まず「最新情報メモ」だけ置いておくね📝✨(章の内容はこの後でガッツリ!)
- TypeScript 5.9 が現行の安定ライン(公式ブログ&Docsが更新継続) (Microsoft for Developers)
- Node.js は v24 が Active LTS、v22 は Maintenance LTS(2026-01 の更新あり) (Node.js)
- Vite は 7.3.1(npm上の最新版) (npmjs.com)
- Problem Details は RFC 9457(RFC 7807 を obsolete) (RFCエディタ)
- catch の
unknown化は tsconfig の useUnknownInCatchVariables が基本路線🛡️ (TypeScript)
0. この総合演習のゴール🎯💖
「失敗」を 思いつき対応じゃなくて、設計として統一できるようになること!✨ 最終的に、こんな“全部入り”を自分で作れるようにするよ😊
成果物チェックリスト✅📦
- 失敗の棚卸し&分類表📋
- エラーカタログ(code / 表示文言 / 対処)🏷️
- Result型+ Domain / Infra エラー型🎁
- 例外境界(UI/API)+ Problem Details 🧾🚪
- requestIdログ+安全ログ方針🧵🔎🔒
- 失敗ケース中心テスト🧪✨
![総合演習の設計図:エラー設計の全貌を広げる[(./picture/err_model_ts_study_030_blueprint_roll.png)
1. ミニプロジェクトの仕様(小さくてリアル)🛍️✨
1-1. 画面(最低限)🖥️🎀
- グッズ一覧📦(名前・価格・在庫)
- 追加フォーム➕📝
- 購入フォーム💳(商品+個数)
- エラーの見せ方統一(トースト/フォーム/ダイアログ)🔔🧾
1-2. API(最低限)🌐🚪
GET /itemsPOST /items(追加)POST /purchase(購入:予算・在庫・支払いAPI)
1-3. “必ず起こる失敗”を仕込む😈✨
- 予算オーバー💸
- 在庫不足📉
- 入力ミス(個数0、負の価格、空文字)✍️🙅♀️
- 外部支払いAPIが落ちる/遅い/拒否する🌩️⏳🚫
2. まず最初にやる:失敗の棚卸し&分類表📋🗺️
ここが総合演習の「核」だよ🧠🔥 コードを書く前に、失敗を先に出し切る!
2-1. 失敗を30個出す(AIでブースト🤖⚡)
AIプロンプト例🤖📝
- 「推し活グッズ管理で起きる失敗ケースを30個。ユーザー入力、業務ルール、外部API、運用を混ぜて」
- 「それぞれを domain / infra / bug に分類して、理由も1行で」
- 「リトライOK/NGも付けて」
2-2. 分類の型(迷ったらこれ)🧭
- DomainError:ユーザーや業務ルール的に起こりうる失敗(例:予算超え)🙂
- InfraError:外部I/Oの失敗(例:支払いAPIタイムアウト)🌩️
- Bug:本来あり得ない(不変条件違反)🧱⚡
3. エラーカタログ(台帳)を作る🏷️📚✨
「エラー名」じゃなくて、運用できる形にするよ😊
3-1. カタログの項目(最小セット)📌
code:例GDS-001userMessage:画面に出す文言(やさしく💗)logMessage:ログ用の詳細(技術寄り🔧)action:推奨アクション(再入力/再試行/問い合わせ)retryable:再試行OK?🔁
AIプロンプト例🤖📝
- 「DomainError 8個の code と userMessage を、女子大生向けの優しい文体で統一して」
- 「InfraError 8個の retryable を判定して理由も」
4. 型設計:Result と “アプリ標準エラー” を作る🎁🧼
ここで「設計が統一」されるよ✨
4-1. Result型(判別可能ユニオン)🎁🌈
export type Ok<T> = { ok: true; value: T };
export type Err<E> = { ok: false; error: E };
export type Result<T, E> = Ok<T> | Err<E>;
export const ok = <T>(value: T): Ok<T> => ({ ok: true, value });
export const err = <E>(error: E): Err<E> => ({ ok: false, error });
4-2. エラー型(Domain / Infra / Bug)🧱🌩️🙂
ポイントは 判別タグ(kind) を必ず入れること!🏷️
export type DomainError =
| { kind: "domain"; code: "GDS-001"; message: "予算が足りません"; required: number; remaining: number }
| { kind: "domain"; code: "GDS-002"; message: "在庫が足りません"; itemId: string; requested: number; stock: number }
| { kind: "domain"; code: "GDS-003"; message: "入力が不正です"; field: string; reason: string };
export type InfraError =
| { kind: "infra"; code: "GDS-101"; message: "支払いAPIに接続できません"; retryable: true }
| { kind: "infra"; code: "GDS-102"; message: "支払いAPIがタイムアウトしました"; retryable: true; timeoutMs: number }
| { kind: "infra"; code: "GDS-103"; message: "支払いが拒否されました"; retryable: false; reason?: string };
export type BugError =
| { kind: "bug"; code: "GDS-901"; message: "不変条件違反"; detail?: string };
export type AppError = DomainError | InfraError | BugError;
5. unknown を “正規化” する(最後の砦)🛡️🧼
どんな throw が来ても同じ形にするのが目的!✨
(tsconfig の useUnknownInCatchVariables と相性バツグン🛡️) (TypeScript)
export function normalizeUnknown(e: unknown): BugError | InfraError {
if (e instanceof Error) {
// ここで e.cause や name を見て分類してもOK(必要に応じて拡張)
return { kind: "infra", code: "GDS-101", message: "支払いAPIに接続できません", retryable: true };
}
return { kind: "bug", code: "GDS-901", message: "不変条件違反", detail: "throw された値が Error ではありません" };
}
6. APIの例外境界:Problem Details で返す🧾🚪
RFC 9457 に沿って application/problem+json で統一すると、フロントが機械的に扱えて超ラク! (RFCエディタ)
6-1. Problem Details 変換
export type ProblemDetails = {
type: string;
title: string;
status: number;
detail?: string;
instance?: string;
// 拡張:RFC的にOK(好きに足していい)
code?: string;
retryable?: boolean;
fieldErrors?: Record<string, string>;
requestId?: string;
};
export function toProblemDetails(err: AppError, instance: string, requestId: string): ProblemDetails {
if (err.kind === "domain") {
return {
type: `https://example.com/problems/${err.code}`,
title: "入力または業務ルールのエラー",
status: 400,
detail: err.message,
instance,
code: err.code,
requestId,
...(err.code === "GDS-003" ? { fieldErrors: { [err.field]: err.reason } } : {}),
};
}
if (err.kind === "infra") {
return {
type: `https://example.com/problems/${err.code}`,
title: "外部サービスのエラー",
status: err.code === "GDS-103" ? 402 : 503,
detail: err.message,
instance,
code: err.code,
retryable: err.retryable,
requestId,
};
}
return {
type: `https://example.com/problems/${err.code}`,
title: "予期しないエラー",
status: 500,
detail: err.message,
instance,
code: err.code,
requestId,
};
}
7. requestId を通す(ログが“一本道”になる)🧵🚶♀️✨
- API入口で
requestId作る - レスポンスヘッダに
X-Request-Id - ログにも必ず入れる
(AIに「requestIdが全ログに入ってるか監査して」って頼むと便利👮♀️✨)
8. レジリエンス:タイムアウト/キャンセル/リトライ⏳🛑🔁
支払いAPIは「落ちる前提」でいこう🌩️✨
8-1. タイムアウトつき fetch(AbortController)⏳✂️
export async function fetchWithTimeout(url: string, timeoutMs: number): Promise<Response> {
const ac = new AbortController();
const t = setTimeout(() => ac.abort(), timeoutMs);
try {
return await fetch(url, { signal: ac.signal });
} finally {
clearTimeout(t);
}
}
8-2. リトライ条件(超大事)⚖️
- ✅ リトライOK:タイムアウト、ネットワーク切断(多くは一時的)
- ❌ リトライNG:支払い拒否、入力ミス(やっても無駄)
AIプロンプト例🤖📝
- 「この InfraError 一覧のうち、リトライすると地獄になるケースを反例で説明して」😱
9. テストは“失敗中心”で作る🧪💖
最新の流れとして Vitest 4 が安定運用ラインだよ(4.0告知あり) (Vitest)
9-1. 最低限のテスト構成✅
- Domain:予算超え→ DomainError を返す💸
- Domain:在庫不足→ DomainError 📉
- Infra:支払いタイムアウト→ InfraError ⏳
- API:Problem Details の形が崩れない🧾
- ログ:requestId が付く🧵
AIプロンプト例🤖🧪
- 「この purchase の仕様で、落とし穴になりがちな失敗テスト観点を10個」
- 「Vitestで domain の境界値テストを追加して」
10. 進め方:おすすめ“3コマ”進行🎬💖
① 設計コマ(30〜60分)🧠🗺️
- 失敗30個 → 分類 → 重要度
- エラーカタログ10〜20件作る
- UIの表示方針(フォーム/トースト/ダイアログ)を先に決める
② 実装コマ(90〜180分)🧰🔥
- Result型 → Domain処理 → 支払い(失敗注入)
- API境界(normalize → Problem Details)
- requestId → ログへ
③ テスト&運用コマ(60〜120分)🧪🔎
- 失敗中心テストを追加
- ログの危険物(個人情報/秘密)混入チェック🔒
- 「問い合わせ時は requestId を聞く」導線にする📞🧵
11. 最終提出物テンプレ(そのまま使ってOK)📦✨
11-1. 失敗棚卸し&分類表(例)📋
- 失敗:予算超え → domain → GDS-001 → 400 → 再入力
- 失敗:支払いAPIタイムアウト → infra → GDS-102 → 503 → リトライ導線🔁
- 失敗:在庫がマイナスになった → bug → GDS-901 → 500 → 調査
11-2. エラーカタログ(例)🏷️
- GDS-001:予算が足りません(購入数を減らしてね💗)
- GDS-102:混み合ってるみたい…少し待って再試行してね⏳🔁
- GDS-901:ごめんね、内部で想定外が起きたよ🙇♀️(requestId付きで問い合わせ)
12. 仕上げ:AIを“相棒”にするコツ🤝🤖✨
AIは 実装スピードは爆上げできるけど、設計の最終判断はあなたが握るのが大事だよ💗
AIにやらせて強いこと💪
- 失敗ケース列挙(漏れ防止)
- 文言トーン統一(優しい日本語)
- テスト観点の追加
- リトライ反例の発掘😱
AIに任せない方がいいこと🙅♀️
- 「どの失敗を Domain にするか」の最終決定
- エラーコード体系の採用(運用に直結)
- “握りつぶし”の正当化(AIはたまにやる😂)
13. クリア条件(合格ライン)🎓✅💖
- 例外境界が 1箇所に集約されてる🚪
- unknown を 正規化できる🧼
- Domain/Infra/Bug が混ざってない🧭
- UIが Problem Details を読んで機械的に表示を分ける🎀
- requestId が「ログ」と「レスポンス」に必ずいる🧵
- 失敗中心テストが最低5本ある🧪
必要なら、この章の内容をそのまま“教材ページ化”できるように、
- エラーカタログの雛形(Markdown)📄
- Problem Details の設計シート🧾
- テスト観点チェックリスト✅ も一緒に吐き出せるよ😊💖