第70章:Chain ② TypeScript定番:関数配列=ミドルウェア🧁
ねらい🎯
前処理・検証・割引・通知みたいな「順番に流したい処理」を、関数の配列でキレイに組めるようになるよ✨
この章でできるようになること✅
- 処理を「小さな関数」に分けて、配列で順番に実行できる🧩
- 途中で止めるルールを統一できる🛑
reduce版と、読みやすい版の両方が分かる🌀- テストで「止まる」「順番」「ログ」を確認できる🧪
1. まず困りごと😵💫
やりがちなのが、1つの関数に全部詰め込むパターン👇
async function placeOrder(input: unknown) {
// 検証
if (!input) throw new Error("invalid");
// 在庫チェック
// 割引
// 税計算
// 決済
// 通知
// ログ
// ...増える増える…
}
こうなるとつらいポイントが増えるよね😭
- 手順が長くて読めない📜
- 途中で止めたい条件がバラバラ🧨
- 1工程だけ差し替えたいのに怖い😱
- テストが書きにくい🧪💦
2. 解決アイデア💡
「処理の列」を 関数配列として持って、
- 1個ずつ順番に実行する➡️
- 失敗したらそこで止める🛑
- 必要ならログで見える化👀
…これが ミドルウェアっぽいChain の気持ちよさだよ🍩✨
3. 直列パイプラインの基本形🧁
いちばん素直で、読みやすくて、実務でも超使える形✨
3.1 途中で止めるためのResult型🧯
「成功/失敗」を戻り値で統一するよ👇
export type Result<T> =
| { ok: true; value: T }
| { ok: false; error: string };
export const ok = <T>(value: T): Result<T> => ({ ok: true, value });
export const err = <T = never>(error: string): Result<T> => ({ ok: false, error });
3.2 Stepの型とrunPipeline🧠
「入力(ctx)を受けて、Resultで返す関数」がStep✨
export type Step<T> = (ctx: T) => Result<T> | Promise<Result<T>>;
export async function runPipeline<T>(steps: Step<T>[], initial: T): Promise<Result<T>> {
let current = ok(initial);
for (const step of steps) {
// どこかで失敗してたら、その時点で止める🛑
if (!current.ok) return current;
current = await step(current.value);
}
return current;
}
4. ハンズオン題材☕🧾
「注文を確定するまで」をチェーン化してみよう✨
4.1 データ型🧩
export type MemberRank = "guest" | "silver" | "gold";
export type OrderItem = {
id: string;
name: string;
price: number;
qty: number;
};
export type Order = {
items: OrderItem[];
memberRank: MemberRank;
couponCode?: string;
};
export type OrderCtx = {
order: Order;
subtotal: number;
discount: number;
tax: number;
total: number;
logs: string[];
};
4.2 Stepを小さく作る🔧
① 検証ステップ✅
import { ok, err, type Step } from "./pipeline";
export const validateItems: Step<OrderCtx> = (ctx) => {
if (ctx.order.items.length === 0) return err("商品が入ってないよ🥺");
return ok({ ...ctx, logs: [...ctx.logs, "validateItems✅"] });
};
② 小計ステップ💰
import { ok, type Step } from "./pipeline";
export const calcSubtotal: Step<OrderCtx> = (ctx) => {
const subtotal = ctx.order.items.reduce((sum, it) => sum + it.price * it.qty, 0);
return ok({ ...ctx, subtotal, logs: [...ctx.logs, "calcSubtotal💰"] });
};
③ 割引ステップ🎟️
import { ok, type Step } from "./pipeline";
const rankRate = (rank: "guest" | "silver" | "gold") => {
if (rank === "gold") return 0.1;
if (rank === "silver") return 0.05;
return 0;
};
export const applyMemberDiscount: Step<OrderCtx> = (ctx) => {
const rate = rankRate(ctx.order.memberRank);
const discount = Math.floor(ctx.subtotal * rate);
return ok({ ...ctx, discount, logs: [...ctx.logs, `applyMemberDiscount🎟️(${rate})`] });
};
④ 税と合計ステップ🧾
import { ok, type Step } from "./pipeline";
export const calcTaxAndTotal: Step<OrderCtx> = (ctx) => {
const taxable = ctx.subtotal - ctx.discount;
const tax = Math.floor(taxable * 0.1);
const total = taxable + tax;
return ok({ ...ctx, tax, total, logs: [...ctx.logs, "calcTaxAndTotal🧾"] });
};
4.3 実行してみる🚀
import { runPipeline } from "./pipeline";
import { validateItems } from "./steps/validateItems";
import { calcSubtotal } from "./steps/calcSubtotal";
import { applyMemberDiscount } from "./steps/applyMemberDiscount";
import { calcTaxAndTotal } from "./steps/calcTaxAndTotal";
import type { OrderCtx } from "./types";
const initial: OrderCtx = {
order: {
memberRank: "gold",
items: [{ id: "latte", name: "カフェラテ", price: 520, qty: 2 }],
},
subtotal: 0,
discount: 0,
tax: 0,
total: 0,
logs: [],
};
const steps = [validateItems, calcSubtotal, applyMemberDiscount, calcTaxAndTotal];
const result = await runPipeline(steps, initial);
if (result.ok) {
console.log("合計:", result.value.total);
console.log(result.value.logs.join(" -> "));
} else {
console.log("失敗:", result.error);
}
5. reduceで書くパイプライン🌀
「reduceで書ける?」って聞かれがちなので、ちゃんと載せるよ😆 ただし、読みやすさは for..of のほうが勝ちになりがち!
import { ok, type Result, type Step } from "./pipeline";
export function runPipelineWithReduce<T>(steps: Step<T>[], initial: T): Promise<Result<T>> {
return steps.reduce(
async (accP, step) => {
const acc = await accP;
if (!acc.ok) return acc;
return step(acc.value);
},
Promise.resolve(ok(initial)),
);
}
6. next()型ミドルウェアのイメージ🧅

「前後に挟みたい」時に強いのが next() スタイル✨
(Webだと GitHub 上の色んなフレームワークでもおなじみの考え方だよ〜📣)
next() を呼ぶと次へ進む、呼ばないと止まる、って発想👣
この「止められる」がChainっぽさだね🛑
next() の考え方はExpressの説明でも出てくるよ。(Express)
6.1 composeの最小版🧩
export type Middleware<C> = (ctx: C, next: () => Promise<void>) => Promise<void>;
export function compose<C>(middlewares: Middleware<C>[]) {
return async (ctx: C) => {
let index = -1;
const dispatch = async (i: number): Promise<void> => {
if (i <= index) throw new Error("next() called multiple times 😵");
index = i;
const fn = middlewares[i];
if (!fn) return;
await fn(ctx, () => dispatch(i + 1));
};
await dispatch(0);
};
}
6.2 例:計測ミドルウェア⏱️
import type { Middleware } from "./compose";
import type { OrderCtx } from "./types";
export const withTiming: Middleware<OrderCtx> = async (ctx, next) => {
const start = Date.now();
await next();
const ms = Date.now() - start;
ctx.logs.push(`withTiming⏱️ ${ms}ms`);
};
7. 途中で止める条件を統一しよう🛑
おすすめはこの2つだけ覚えればOK✨
- 直列パイプラインなら Resultで止める(この章のメイン)✅
- next型なら nextを呼ばない(止めるルールを決める)🧅
「途中で止める」を if で好き勝手にやりだすと、すぐカオスになるよ〜😭
8. テストで安心を作る🧪
Nodeには標準のテスト機能 node:test があって、Node 20で安定扱いになった流れがあるよ。(Node.js)
8.1 テスト例✅
import test from "node:test";
import assert from "node:assert/strict";
import { runPipeline } from "./pipeline";
import { validateItems } from "./steps/validateItems";
import { calcSubtotal } from "./steps/calcSubtotal";
import { applyMemberDiscount } from "./steps/applyMemberDiscount";
import { calcTaxAndTotal } from "./steps/calcTaxAndTotal";
import type { OrderCtx } from "./types";
test("成功すると合計が計算される🎉", async () => {
const initial: OrderCtx = {
order: { memberRank: "gold", items: [{ id: "latte", name: "ラテ", price: 500, qty: 2 }] },
subtotal: 0,
discount: 0,
tax: 0,
total: 0,
logs: [],
};
const steps = [validateItems, calcSubtotal, applyMemberDiscount, calcTaxAndTotal];
const result = await runPipeline(steps, initial);
assert.equal(result.ok, true);
if (result.ok) {
assert.equal(result.value.subtotal, 1000);
assert.ok(result.value.total > 0);
assert.deepEqual(result.value.logs[0], "validateItems✅");
}
});
test("商品が空なら途中で止まる🛑", async () => {
const initial: OrderCtx = {
order: { memberRank: "guest", items: [] },
subtotal: 0,
discount: 0,
tax: 0,
total: 0,
logs: [],
};
const steps = [validateItems, calcSubtotal, applyMemberDiscount, calcTaxAndTotal];
const result = await runPipeline(steps, initial);
assert.equal(result.ok, false);
if (!result.ok) {
assert.match(result.error, /商品が入ってない/);
}
});
9. よくあるつまずきポイント💡
- Stepの形がバラバラになる😵
👉
(ctx)=>Resultに揃えるだけで世界が平和🕊️ - ctxを破壊的に更新するか、コピーして返すかが混ざる🌀 👉 どっちでもいいけど、混ぜないのが大事!
- 順番依存が隠れる🙈 👉 配列の上にコメントで「なぜこの順?」を書くだけで神✨
- next型で next()を2回呼ぶ💥 👉 composeで検知して落とす(上の実装みたいに)
10. AIプロンプト例🤖💬
長い関数を「関数配列のpipeline」に分割したいです。
- 1ステップの責務が小さくなるように分割
- Step型は (ctx)=>Result<ctx> に統一
- 途中で止める条件もResultで統一
- ついでにテストケースも提案して
対象コード: <ここに貼る>
このpipelineのsteps配列の順番をレビューして。
- 依存関係(どの値がどのステップで作られるか)
- 途中停止の条件
- 追加しやすい拡張ポイント
も指摘して。