🍛

TypeScriptのTips - カリー化で業務ルールと処理を分離する

に公開
2026/06/12

自分がコーディングで使っている、ちょっとしたテクニックを紹介します。

今回のテーマは カリー化 です。
このテクニックを使うことで、業務ルールと処理を分離し再利用性・テスト容易性が向上します。

サンプルとして、手数料を計算する処理を題材にします。
要件は以下の通りとします。

  • 関数は金額を受け取り、適用する手数料を返す
  • 手数料は価格帯によって決定する
    • 0 ~ 499円は手数料 300円
    • 500 ~ 1999円は手数料 50円
    • 2000円以上は手数料なし
  • 場合によっては手数料が複数かかる場合もある

よくある実装例

まずは、よくある実装例です。
(そもそもハードコードがよくない、などは目を瞑ってほしい。)

type FeeRule = {
  min: number;
  max: number | null;
  fee: number;
};

const calculateFee = (amount: number): number => {
  const feeRules: FeeRule[] = [
    { min: 0, max: 499, fee: 300 },
    { min: 500, max: 1999, fee: 50 },
    { min: 2000, max: null, fee: 0 },
  ];

  return feeRules
    .filter(rule =>
      amount >= rule.min &&
      (rule.max === null || amount <= rule.max)
    )
    .reduce((sum, rule) => sum + rule.fee, 0);
};

要件は満たしてるしパッとみ問題なさそうに見えますが、いくつかの改善点があります。
今回はこの関数をカリー化という手法でリファクタリングしてみます。

カリー化後の実装

type FeeRule = {
  min: number;
  max: number | null;
  fee: number;
};

// カリー化して手数料ルールを引数から注入する。処理自体は具体的な業務ルールを知らない。
const createFeeCalculator =
  (rules: FeeRule[]) =>
  (amount: number): number =>
    rules
      .filter(rule =>
        amount >= rule.min &&
        (rule.max === null || amount <= rule.max)
      )
      .reduce((sum, rule) => sum + rule.fee, 0);

const feeRules = [
  { min: 0, max: 499, fee: 300 },
  { min: 500, max: 1999, fee: 50 },
  { min: 2000, max: null, fee: 0 },
] as const satisfies FeeRule[];

const calculateFee = createFeeCalculator(feeRules)

変更点はcreateFeeCalculator内で宣言していたfeeRulesを引数で注入するようにしたことです。
これによって処理と業務ルールが明確に分離されました。

const createFeeCalculator =
  (rules: FeeRule[]) =>
  (amount: number): number => {
    /* ... */
  };

この関数が知っているのは、

  • 金額の比較方法
  • 条件に合うルールの抽出方法
  • 手数料の合算方法

といった 計算の仕方そのもの のみで、具体的な金額の閾値は知りません。

一方で

const feeRules = [
  { min: 0, max: 499, fee: 300 },
  { min: 500, max: 1999, fee: 50 },
  { min: 2000, max: null, fee: 0 },
];

こちらは、

  • どの価格帯で
  • いくら手数料を取るか

という 業務ルールそのもの を表しています。

このように処理と業務ルールを分離することで再利用性・テスト容易性が高まります。

再利用性

処理と業務ルールが分離されていることで、処理の再利用性が高まります。
例えば、プレミアム会員の場合に手数料ルールが変わる、という要件変更を考えます。

カリー化前のコードでは、処理と業務ルールが一体化しているために処理自体を2重に実装したり、改修したりする必要があります。これはDRYに違反したり、既存の処理を破壊する可能性を伴います。
カリー化後のコードでは、createFeeCalculatorの引数に渡すfeeRulesを変更するだけで済みます。

// リファクタリング後
type FeeRule = {
  min: number;
  max: number | null;
  fee: number;
};
const createFeeCalculator =
  (rules: FeeRule[]) =>
  (amount: number): number =>
    rules
      .filter(rule =>
        amount >= rule.min &&
        (rule.max === null || amount <= rule.max)
      )
      .reduce((sum, rule) => sum + rule.fee, 0);

const feeRules = [
  { min: 0, max: 499, fee: 300 },
  { min: 500, max: 1999, fee: 50 },
  { min: 2000, max: null, fee: 0 },
] as const satisfies FeeRule[];
const calculateFee = createFeeCalculator(feeRules)

// 以下を追加するだけで対応できる
const feeRulesForPremium = [
  { min: 0, max: 499, fee: 300 },
  { min: 500, max: null, fee: 0 },
] as const satisfies FeeRule[];

// プレミアム会員向けの手数料を計算する
const calculateFeeForPremiumMember = createFeeCalculator(feeRulesForPremium)

テスト容易性

カリー化前は処理と業務ルールが一体になっているため、現状の業務ルールに合わせたテストコードを書くことになります。
一方カリー化後は処理が業務ルールと分離されてるため、テスト用の手数料ルール(feeRules)を注入しテストできます。

テスト用に手数料ルールを注入できると

  • 現状の業務ルールでは再現できないケースもテストできる
  • 業務ルール変更の影響を処理のテストコードが受けない

のようなメリットがあります。

現状の業務ルールではある価格帯に対して手数料が単一です。
そのためリファクタリング前のテストコードでは、ある価格帯に対して複数の手数料ルールがかかるときにどうなるか?のテストを書くことはできません。
一方リファクタリング後はテスト用の手数料ルールを注入できるため、現在の業務ルールでは再現しないエッジケースなどもテスト可能です。
これは将来的な不具合の予防につながります。

また、例えば手数料300円の価格帯を0~1000円に変更する場合に、
カリー化前は関数が内包する業務ルールの境界値が変わるためテストコードも修正が必要になります。
カリー化後は、処理のテストにはテスト用の手数料ルールを注入してるため、業務ルール都合で処理に対するテストコードの修正が不要になります。

まとめ

今回紹介したカリー化のテクニックを使い業務ルールと処理を分離することで、コードの再利用性・テスト容易性が高まります。
テストがしやすく再利用しやすくなるということは、コードの変更が容易になることを意味します。

このテクニックは例えば以下のようなケースにも適用可能で、意外と活用できる機会は多いです。

  • バリデーションの違反チェック処理と、各項目のバリデーションルール
  • 権限チェック処理と、各ロールがもつ権限ルール
  • 校閲用のワード検閲処理と、NGワード

どこかで参考になれば嬉しいです。

Discussion