JavaScript reduceを使った業務実装パターン|実務で役立つ配列処理テクニック

未分類

JavaScript reduce を使った業務実装パターン|実務で役立つ配列処理テクニック

JavaScriptのreduceメソッドは、配列を処理して単一の値を生成する強力なツールです。しかし、実務ではどのように活用すればよいのか、具体的なシーンが思いつかない開発者も多いのではないでしょうか。本記事では、実務でよく出現するreduceのユースケースを、実装コードとともに解説します。

reduceメソッドの簡易解説

reduceメソッドは、配列の各要素に対して関数を実行し、その結果を次のイテレーションへ受け渡すことで、最終的に1つの値を生成します。

const result = array.reduce((accumulator, currentValue, index, array) => {
  // accumulatorに値を蓄積していく
  return accumulator;
}, initialValue);

各パラメータの意味:

  • accumulator:前回のコールバック実行の戻り値、または初期値
  • currentValue:処理中の現在の配列要素
  • index:現在処理中の要素のインデックス
  • initialValue:accumulatorの初期値(省略可)

業務でのユースケース

ユースケース1:数値の合計・集計

最も基本的で頻出のパターンです。ECサイトの売上集計やユーザーのスコア計算などで使用します。

// 商品の販売金額を集計する例
const sales = [
  { id: 1, amount: 5000, category: 'electronics' },
  { id: 2, amount: 3000, category: 'books' },
  { id: 3, amount: 7500, category: 'electronics' },
  { id: 4, amount: 2000, category: 'books' }
];

// 全体の売上を計算
const totalSales = sales.reduce((sum, item) => sum + item.amount, 0);
console.log(totalSales); // 17500

// カテゴリー別の売上を集計
const salesByCategory = sales.reduce((acc, item) => {
  acc[item.category] = (acc[item.category] || 0) + item.amount;
  return acc;
}, {});
console.log(salesByCategory); 
// { electronics: 12500, books: 5000 }

ユースケース2:配列をオブジェクトに変換

APIレスポンスのデータを、IDをキーとしたオブジェクトに変換する際に活躍します。検索や更新処理が効率化されます。

// ユーザー一覧をIDをキーにしたオブジェクトへ変換
const users = [
  { id: 101, name: '田中太郎', department: '営業' },
  { id: 102, name: '鈴木花子', department: 'エンジニア' },
  { id: 103, name: '佐藤次郎', department: '営業' }
];

const usersById = users.reduce((acc, user) => {
  acc[user.id] = user;
  return acc;
}, {});

console.log(usersById[102]); 
// { id: 102, name: '鈴木花子', department: 'エンジニア' }

// さらに、特定フィールドのみを抽出する場合
const userNames = users.reduce((acc, user) => {
  acc[user.id] = user.name;
  return acc;
}, {});

console.log(userNames); 
// { 101: '田中太郎', 102: '鈴木花子', 103: '佐藤次郎' }

ユースケース3:配列のフィルタリングと変換を同時に行う

データベースクエリのフィルタリングとマッピングを一度に処理することで、コード量を削減し、パフォーマンスも向上させます。

// 注文データから、特定条件を満たす項目を抽出・変換
const orders = [
  { id: 1, status: 'completed', total: 10000, customerId: 5 },
  { id: 2, status: 'pending', total: 5000, customerId: 10 },
  { id: 3, status: 'completed', total: 15000, customerId: 5 },
  { id: 4, status: 'cancelled', total: 3000, customerId: 15 },
  { id: 5, status: 'completed', total: 8000, customerId: 10 }
];

// ステータスが'completed'の注文を抽出し、顧客IDと金額のみにする
const completedOrderSummary = orders.reduce((acc, order) => {
  if (order.status === 'completed') {
    acc.push({
      customerId: order.customerId,
      amount: order.total
    });
  }
  return acc;
}, []);

console.log(completedOrderSummary);
// [
//   { customerId: 5, amount: 10000 },
//   { customerId: 5, amount: 15000 },
//   { customerId: 10, amount: 8000 }
// ]

ユースケース4:グループ化処理

データを複数のグループに分類する際に、reduceは非常に強力です。レポーティング機能やダッシュボード作成で頻用されます。

// 従業員データを部門別にグループ化
const employees = [
  { name: '田中太郎', department: '営業', salary: 350000 },
  { name: '鈴木花子', department: 'エンジニア', salary: 450000 },
  { name: '佐藤次郎', department: '営業', salary: 380000 },
  { name: '伊藤美咲', department: 'エンジニア', salary: 480000 },
  { name: '加藤健太', department: '企画', salary: 400000 }
];

const groupedByDept = employees.reduce((acc, emp) => {
  const dept = emp.department;
  if (!acc[dept]) {
    acc[dept] = [];
  }
  acc[dept].push(emp);
  return acc;
}, {});

console.log(groupedByDept);
// {
//   営業: [
//     { name: '田中太郎', department: '営業', salary: 350000 },
//     { name: '佐藤次郎', department: '営業', salary: 380000 }
//   ],
//   エンジニア: [
//     { name: '鈴木花子', department: 'エンジニア', salary: 450000 },
//     { name: '伊藤美咲', department: 'エンジニア', salary: 480000 }
//   ],
//   企画: [
//     { name: '加藤健太', department: '企画', salary: 400000 }
//   ]
// }

実装コード:実務での活用例

ここでは、eコマースプラットフォームでよく出現するシーンを想定したコードを紹介します。

例:注文集計レポートの生成

// 日次の注文データから複数の集計値を一度に算出
const dailyOrders = [
  { orderId: 1, amount: 12000, status: 'completed', paymentMethod: 'card' },
  { orderId: 2, amount: 5500, status: 'completed', paymentMethod: 'bank' },
  { orderId: 3, amount: 0, status: 'cancelled', paymentMethod: 'card' },
  { orderId: 4, amount: 8300, status: 'completed', paymentMethod: 'card' },
  { orderId: 5, amount: 3200, status: 'pending', paymentMethod: 'bank' }
];

const report = dailyOrders.reduce((acc, order) => {
  // 全体の統計
  acc.totalOrders++;
  
  if (order.status === 'completed') {
    acc.completedOrders++;
    acc.totalRevenue += order.amount;
    
    // 支払い方法別の集計
    if (!acc.paymentMethodBreakdown[order.paymentMethod]) {
      acc.paymentMethodBreakdown[order.paymentMethod] = 0;
    }
    acc.paymentMethodBreakdown[order.paymentMethod]++;
  }
  
  if (order.status === 'cancelled') {
    acc.cancelledOrders++;
  }
  
  if (order.status === 'pending') {
    acc.pendingOrders++;
  }
  
  return acc;
}, {
  totalOrders: 0,
  completedOrders: 0,
  cancelledOrders: 0,
  pendingOrders: 0,
  totalRevenue: 0,
  paymentMethodBreakdown: {}
});

console.log(report);
// {
//   totalOrders: 5,
//   completedOrders: 3,
//   cancelledOrders: 1,
//   pendingOrders: 1,
//   totalRevenue: 25800,
//   paymentMethodBreakdown: { card: 2, bank: 1 }
// }

例:データの階層変換(TypeScript版)

TypeScriptを使用した実務例です。型安全性を確保しながらreduceを使用できます。

interface Product {
  id: number;
  name: string;
  price: number;
  inStock: boolean;
}

interface ProductCatalog {
  available: Product[];
  outOfStock: Product[];
  totalValue: number;
}

const products: Product[] = [
  { id: 1, name: 'ノートPC', price: 120000, inStock: true },
  { id: 2, name: 'マウス', price: 3000, inStock: false },
  { id: 3, name: 'キーボード', price: 8000, inStock: true },
  { id: 4, name: 'モニター', price: 35000, inStock: false }
];

const catalog: ProductCatalog = products.reduce(
  (acc, product) => {
    if (product.inStock) {
      acc.available.push(product);
    } else {
      acc.outOfStock.push(product);
    }
    acc.totalValue += product.price;
    return acc;
  },
  {
    available: [],
    outOfStock: [],
    totalValue: 0
  }
);

console.log(catalog);
// {
//   available: [
//     { id: 1, name: 'ノートPC', price: 120000, inStock: true },
//     { id: 3, name: 'キーボード', price: 8000, inStock: true }
//   ],
//   outOfStock: [
//     { id: 2, name: 'マウス', price: 3000, inStock: false },
//     { id: 4, name: 'モニター', price: 35000, inStock: false }
//   ],
//   totalValue: 166000
// }

よくある応用パターン

パターン1:配列の重複排除

reduceを使って配列から重複要素を効率的に除去できます。

const ids = [1, 2, 3, 2, 4, 1, 5, 3];

// オブジェクトをaccumulatorにして重複判定
const uniqueIds = ids.reduce((acc, id) => {
  if (!acc.seen[id]) {
    acc.seen[id] = true;
    acc.result.push(id);
  }
  return acc;
}, { seen: {}, result: [] }).result;

console.log(uniqueIds); // [1, 2, 3, 4, 5]

// より簡潔な方法
const uniqueIds2 = ids.reduce((acc, id) => 
  acc.includes(id) ? acc : [...acc, id], []);

console.log(uniqueIds2); // [1, 2, 3, 4, 5]

パターン2:複数の値を並行して処理

reduceで複数の集計値を一度に計算することで、ループ回数を減らしパフォーマンスを向上させます。

const scores = [85, 92, 78, 95, 88, 73, 91];

const statistics = scores.reduce((acc, score) => {
  acc.sum += score;
  acc.count++;
  acc.max = Math.max(acc.max, score);
  acc.min = Math.min(acc.min, score);
  return acc;
}, { sum: 0, count: 0, max: 0, min: 100 });

statistics.average = statistics.sum / statistics.count;

console.log(statistics);
// {
//   sum: 602,
//   count: 7,
//   max: 95,
//   min: 73,
//   average: 86
// }

パターン3:条件付きの複数値変換

// eコマースの注文から、各ステータスごとに合計金額と商品数を算出
const transactions = [
  { status: 'delivered', items: 2, total: 8500 },
  { status: 'pending', items: 1, total: 3000 },
  { status: 'delivered', items: 3, total: 15200 },
  { status: 'cancelled', items: 1, total: 2000 },
  { status: 'pending', items: 2, total: 9800 }
];

const statusAnalysis = transactions.reduce((acc, txn) => {
  const status = txn.status;
  
  if (!acc[status]) {
    acc[status] = { count: 0, totalItems: 0, totalAmount: 0 };
  }
  
  acc[status].count++;
  acc[status].totalItems += txn.items;
  acc[status].totalAmount += txn.total;
  
  return acc;
}, {});

console.log(statusAnalysis);
// {
//   delivered: { count: 2, totalItems: 5, totalAmount: 23700 },
//   pending: { count: 2, totalItems: 3, totalAmount: 12800 },
//   cancelled: { count: 1, totalItems: 1, totalAmount: 2000 }
// }

パフォーマンス面での注意点

reduceは強力ですが、パフォーマンスに関する注意点があります。

注意点1:配列のスプレッド演算子の使用

accumulatorが配列の場合、スプレッド演算子(...acc)を使うと毎回新しい配列が生成されるため、大規模データでは遅くなります。

// ❌ 遅い方法:毎回配列を複製
const slowResult = items.reduce((acc, item) => {
  return item.active ? [...acc, item] : acc;
}, []);

// ✅ 速い方法:既存の配列にpushする
const fastResult = items.reduce((acc, item) => {
  if (item.active) {
    acc.push(item);
  }
  return acc;
}, []);

注意点2:ネストが深い場合の可読性

複雑な処理をreduce内に記述すると、可読性が低下します。処理が複雑な場合は関数を分割することをお勧めします。

// ❌ 可読性が低い
const result = data.reduce((acc, item) => {
  if (item.type === 'A' && item.value > 100) {
    acc[item.category] = (acc[item.category] || 0) + item.value;
  }
  return acc;
}, {});

// ✅ より読みやすい
const isValidItem = (item) => item.type === 'A' && item.value > 100;

const addToCategory = (acc, item) => {
  acc[item.category] = (acc[item.category] || 0) + item.value;
  return acc;
};

const result = data
  .filter(isValidItem)
  .reduce(addToCategory, {});

注意点3:初期値の省略による問題

初期値を省略すると、空配列で予期しないエラーが発生する可能性があります。

// ❌ 危険:空配列でエラー
const sum = [].reduce((a, b) => a + b); // TypeError

// ✅ 安全:初期値を指定
const sum = [].reduce((a, b) => a + b, 0); // 0

Python版での実装参考

Pythonでも同様の処理が必要な場合、functools.reduceを使用できます。ただし、Pythonではリスト内包表記やdictメソッドを使う方が慣例的です。

from functools import reduce

# 売上集計の例
sales = [
    {'id': 1, 'amount': 5000, 'category': 'electronics'},
    {'id': 2, 'amount': 3000, 'category': 'books'},
    {'id': 3, 'amount': 7500, 'category': 'electronics'}
]

# reduceを使った方法
total = reduce(lambda acc, item: acc + item['amount'], sales, 0)
print(total)  # 15500

# より Pythonic な方法(推奨)
total_pythonic = sum(item['amount'] for item in sales)
print(total_pythonic)  # 15500

# カテゴリー別集計(dictのgetメソッド推奨)
from collections import defaultdict

sales_by_category = defaultdict(int)
for item in sales:
    sales_by_category[item['category']] += item['amount']

print(dict(sales_by_category))
# {'electronics': 12500, 'books': 3000}

まとめ

JavaScriptのreduceメソッドは、配列処理における強力なツールです。数値の合計、オブジェクトへの変換、データのグループ化、複数値の並行計算など、業務で頻出するシーンに対応できます。

reduceを使うべき場面:

  • 配列から単一の値を生成したい
  • 複数の処理を一度のループで完結させたい
  • 複雑なデータ構造を組み立てたい

reduceを避けるべき場面:

  • 単純なフィルタリングはfilterで十分
  • 各要素に処理を施すだけならmapで十分
  • 処理が複雑で可読性が低下する場合

実務では、reduceの強力さと可読性のバランスを取ることが重要です。上記のコード例を参考に、プロジェクトの要件に応じて適切に活用してください。

タイトルとURLをコピーしました