reduce関数の実践的な使い方|実務で使える業務パターン集
プログラミングを学ぶ過程で「reduce関数は難しい」という評判をよく聞きます。しかし実務では、reduceこそが最も効果的にデータを処理できる強力なツールです。本記事では教科書的な説明ではなく、実際の業務で日々使われるパターンを中心に解説します。
reduceの基本概念
reduceは配列のすべての要素に対して関数を実行し、単一の値に集約する関数です。
// 基本形式\nconst result = array.reduce((accumulator, currentValue, index, array) => {\n // 処理\n return accumulator; // 次の反復に渡す値\n}, initialValue);
TypeScriptの型定義:
reduce<U>(\n callback: (previousValue: U, currentValue: T, currentIndex: number, array: T[]) => U,\n initialValue: U\n): U
Pythonでは関数型プログラミングの「functools.reduce」として提供されます:
from functools import reduce\nresult = reduce(lambda acc, val: acc + val, [1, 2, 3, 4], 0)
実務で頻出するユースケース
1. データの集計・合計
最も基本的かつ頻繁に使用するパターンです。単なる合計ではなく、条件付き集計が実務では多いです。
// 実務例:売上データから特定条件の合計を計算
interface SalesRecord {\n date: string;\n amount: number;\n status: 'completed' | 'pending' | 'cancelled';\n category: string;\n}\n\nconst salesData: SalesRecord[] = [\n { date: '2024-01-15', amount: 50000, status: 'completed', category: 'A' },\n { date: '2024-01-16', amount: 30000, status: 'pending', category: 'B' },\n { date: '2024-01-17', amount: 80000, status: 'completed', category: 'A' },\n { date: '2024-01-18', amount: 20000, status: 'cancelled', category: 'C' },\n { date: '2024-01-19', amount: 60000, status: 'completed', category: 'B' },\n];\n\n// 完了済みの売上のみ集計\nconst completedSalesTotal = salesData.reduce((sum, record) => {\n return record.status === 'completed' ? sum + record.amount : sum;\n}, 0);\n\nconsole.log(completedSalesTotal); // 190000
Pythonの同等実装:
from functools import reduce\nfrom typing import List, TypedDict\n\nclass SalesRecord(TypedDict):\n date: str\n amount: int\n status: str\n category: str\n\nsales_data: List[SalesRecord] = [\n {'date': '2024-01-15', 'amount': 50000, 'status': 'completed', 'category': 'A'},\n {'date': '2024-01-16', 'amount': 30000, 'status': 'pending', 'category': 'B'},\n {'date': '2024-01-17', 'amount': 80000, 'status': 'completed', 'category': 'A'},\n {'date': '2024-01-18', 'amount': 20000, 'status': 'cancelled', 'category': 'C'},\n {'date': '2024-01-19', 'amount': 60000, 'status': 'completed', 'category': 'B'},\n]\n\ncompleted_total = reduce(\n lambda acc, record: acc + record['amount'] if record['status'] == 'completed' else acc,\n sales_data,\n 0\n)\n\nprint(completed_total) # 190000
2. オブジェクトの構築・変換
配列からオブジェクトを生成する処理は実務で非常に多いです。データベース結果の整形、APIレスポンスの変換など様々な場面で活躍します。
// 実務例:ユーザーデータをIDをキーとしたオブジェクトマップに変換\ninterface User {\n id: number;\n name: string;\n email: string;\n department: string;\n}\n\nconst users: User[] = [\n { id: 101, name: '田中太郎', email: 'tanaka@example.com', department: '営業' },\n { id: 102, name: '佐藤花子', email: 'sato@example.com', department: 'マーケ' },\n { id: 103, name: '鈴木次郎', email: 'suzuki@example.com', department: 'IT' },\n];\n\n// IDをキーとしたマップを生成\nconst userMap = users.reduce>((map, user) => {\n map[user.id] = user;\n return map;\n}, {});\n\nconsole.log(userMap[102]); // { id: 102, name: '佐藤花子', ... }\n\n// さらに高度な例:複数のキーでのグループ化\nconst usersByDepartment = users.reduce>((map, user) => {\n if (!map[user.department]) {\n map[user.department] = [];\n }\n map[user.department].push(user);\n return map;\n}, {});\n\nconsole.log(usersByDepartment['IT']); // IT部門のユーザー一覧
Pythonの実装:
from functools import reduce\nfrom typing import Dict, List, TypedDict\n\nclass User(TypedDict):\n id: int\n name: str\n email: str\n department: str\n\nusers: List[User] = [\n {'id': 101, 'name': '田中太郎', 'email': 'tanaka@example.com', 'department': '営業'},\n {'id': 102, 'name': '佐藤花子', 'email': 'sato@example.com', 'department': 'マーケ'},\n {'id': 103, 'name': '鈴木次郎', 'email': 'suzuki@example.com', 'department': 'IT'},\n]\n\n# IDをキーとしたマップを生成\nuser_map: Dict[int, User] = reduce(\n lambda acc, user: {**acc, user['id']: user},\n users,\n {}\n)\n\nprint(user_map[102])\n\n# 部門でグループ化\nusers_by_dept: Dict[str, List[User]] = reduce(\n lambda acc, user: {\n **acc,\n user['department']: acc.get(user['department'], []) + [user]\n },\n users,\n {}\n)\n\nprint(users_by_dept['IT'])
3. 複雑なフィルタリングと変換の組み合わせ
実務では単なるフィルタリングではなく、条件に応じた変換が必要な場合がほとんどです。reduceなら1パスで処理できます。
// 実務例:注文データから統計情報を抽出\ninterface Order {\n id: string;\n customerId: number;\n amount: number;\n items: number;\n createdAt: Date;\n status: 'pending' | 'shipped' | 'delivered' | 'returned';\n}\n\nconst orders: Order[] = [\n { id: 'ORD001', customerId: 1001, amount: 15000, items: 3, createdAt: new Date('2024-01-10'), status: 'delivered' },\n { id: 'ORD002', customerId: 1002, amount: 8000, items: 1, createdAt: new Date('2024-01-12'), status: 'shipped' },\n { id: 'ORD003', customerId: 1001, amount: 25000, items: 5, createdAt: new Date('2024-01-15'), status: 'delivered' },\n { id: 'ORD004', customerId: 1003, amount: 5000, items: 1, createdAt: new Date('2024-01-18'), status: 'pending' },\n { id: 'ORD005', customerId: 1002, amount: 12000, items: 2, createdAt: new Date('2024-01-20'), status: 'returned' },\n];\n\ninterface OrderStats {\n totalRevenue: number;\n deliveredCount: number;\n averageOrderValue: number;\n totalItems: number;\n returnsCount: number;\n topCustomerId?: number;\n topCustomerSpent?: number;\n}\n\n// 1パスで複数の統計情報を計算\nconst stats = orders.reduce(\n (acc, order) => {\n const isDelivered = order.status === 'delivered';\n const isReturned = order.status === 'returned';\n const isCompleted = isDelivered || order.status === 'shipped';\n\n return {\n totalRevenue: isCompleted ? acc.totalRevenue + order.amount : acc.totalRevenue,\n deliveredCount: isDelivered ? acc.deliveredCount + 1 : acc.deliveredCount,\n totalItems: acc.totalItems + (isCompleted ? order.items : 0),\n averageOrderValue: 0, // 後で計算\n returnsCount: isReturned ? acc.returnsCount + 1 : acc.returnsCount,\n topCustomerId: acc.topCustomerSpent === undefined || order.amount > acc.topCustomerSpent\n ? order.customerId\n : acc.topCustomerId,\n topCustomerSpent: acc.topCustomerSpent === undefined || order.amount > acc.topCustomerSpent\n ? order.amount\n : acc.topCustomerSpent,\n };\n },\n {\n totalRevenue: 0,\n deliveredCount: 0,\n totalItems: 0,\n averageOrderValue: 0,\n returnsCount: 0,\n }\n);\n\n// 平均値を計算\nstats.averageOrderValue = stats.deliveredCount > 0\n ? Math.round(stats.totalRevenue / stats.deliveredCount)\n : 0;\n\nconsole.log(stats);
4. 状態管理と累積処理
特に複雑なビジネスロジックの実装では、reduceが状態管理に最適です。
// 実務例:イベントログから最終状態を計算\ninterface StateEvent {\n timestamp: Date;\n type: 'created' | 'updated' | 'approved' | 'rejected' | 'published';\n changes?: Record;\n}\n\ninterface DocumentState {\n status: 'draft' | 'pending_review' | 'approved' | 'rejected' | 'published';\n lastUpdated: Date;\n approvalCount: number;\n rejectionCount: number;\n metadata: Record;\n}\n\nconst events: StateEvent[] = [\n { timestamp: new Date('2024-01-10T10:00:00'), type: 'created', changes: { title: 'Report Q1' } },\n { timestamp: new Date('2024-01-12T14:30:00'), type: 'updated', changes: { content: 'Updated content' } },\n { timestamp: new Date('2024-01-15T09:00:00'), type: 'approved', changes: { approver: 'manager_a' } },\n { timestamp: new Date('2024-01-15T15:00:00'), type: 'approved', changes: { approver: 'manager_b' } },\n { timestamp: new Date('2024-01-18T11:00:00'), type: 'published', changes: {} },\n];\n\nconst finalState = events.reduce(\n (state, event) => {\n switch (event.type) {\n case 'created':\n return {\n ...state,\n status: 'draft',\n lastUpdated: event.timestamp,\n metadata: event.changes || {},\n };\n case 'updated':\n return {\n ...state,\n lastUpdated: event.timestamp,\n metadata: { ...state.metadata, ...event.changes },\n };\n case 'approved':\n return {\n ...state,\n status: state.approvalCount + 1 >= 2 ? 'approved' : 'pending_review',\n approvalCount: state.approvalCount + 1,\n lastUpdated: event.timestamp,\n };\n case 'rejected':\n return {\n ...state,\n status: 'rejected',\n rejectionCount: state.rejectionCount + 1,\n lastUpdated: event.timestamp,\n };\n case 'published':\n return {\n ...state,\n status: 'published',\n lastUpdated: event.timestamp,\n };\n default:\n return state;\n }\n },\n {\n status: 'draft',\n lastUpdated: new Date(),\n approvalCount: 0,\n rejectionCount: 0,\n metadata: {},\n }\n);\n\nconsole.log(finalState); // 最終状態が得られる
高度な応用パターン
パターン1:チェーン可能な変換(map + filter相当)
// 複数の変換をチェーンする\ninterface Product {\n id: number;\n name: string;\n price: number;\n stock: number;\n category: string;\n}\n\nconst products: Product[] = [\n { id: 1, name: 'ノートPC', price: 150000, stock: 5, category: '電子機器' },\n { id: 2, name: 'マウス', price: 3000, stock: 50, category '電子機器' },\n { id: 3, name: '机', price: 45000, stock: 0, category: '家具' },\n { id: 4, name: 'キーボード', price: 12000, stock: 20, category: '電子機器' },\n];\n\n// 在庫がある商品のみを取得し、税込価格に変換\nconst inStockWithTax = products.reduce>(\n (acc, product) => {\n if (product.stock > 0) {\n acc.push({\n id: product.id,\n name: product.name,\n priceWithTax: Math.round(product.price * 1.1),\n });\n }\n return acc;\n },\n []\n);
パターン2:ネストされたデータ構造の操作
// 実務例:階層化されたメニュー構造の生成\ninterface MenuSource {\n id: number;\n label: string;\n parentId: number | null;\n order: number;\n}\n\ninterface MenuNode {\n id: number;\n label: string;\n order: number;\n children: MenuNode[];\n}\n\nconst menuItems: MenuSource[] = [\n { id: 1, label: 'ホーム', parentId: null, order: 1 },\n { id: 2, label: 'サービス', parentId: null, order: 2 },\n { id: 3, label: 'Web制作', parentId: 2, order: 1 },\n { id: 4, label: 'アプリ開発', parentId: 2, order: 2 },\n { id: 5, label: 'お問い合わせ', parentId: null, order: 3 },\n { id: 6, label: 'コンサルティング', parentId: 2, order: 3 },\n];\n\nconst buildMenuTree = (items: MenuSource[]): MenuNode[] => {\n // まず全アイテムをMapに変換\n const itemMap = items.reduce
パターン3:条件分岐による複数結果の同時計算
// 実務例:レポート生成時に複数の統計を同時計算\ninterface Transaction {\n id: string;\n amount: number;\n type: 'income' | 'expense';\n category: string;\n date: Date;\n}\n\ninterface FinancialReport {\n totalIncome: number;\n totalExpense: number;\n netProfit: number;\n incomeByCategory: Record;\n expenseByCategory: Record;\n monthlyTotals: Record;\n}\n\nconst transactions: Transaction[] = [\n { id: 'TXN001', amount: 500000, type: 'income', category: 'sales', date: new Date('2024-01-05') },\n { id: 'TXN002', amount: 120000, type: 'expense', category: 'salary', date: new Date('2024-01-10') },\n { id: 'TXN003', amount: 800000, type: 'income', category: 'services', date: new Date('2024-01-15') },\n { id: 'TXN004', amount: 50000, type: 'expense', category: 'utilities', date: new Date('2024-01-20') },\n { id: 'TXN005', amount: 100000, type: 'expense', category: 'marketing', date: new Date('2024-02-05') },\n];\n\nconst generateReport = (txns: Transaction[]): FinancialReport => {\n return txns.reduce(\n (report, txn) => {\n const monthKey = txn.date.toISOString().slice(0, 7); // 'YYYY-MM'\n\n if (txn.type === 'income') {\n return {\n ...report,\n totalIncome: report.totalIncome + txn.amount,\n netProfit: report.netProfit + txn.amount,\n incomeByCategory: {\n ...report.incomeByCategory,\n [txn.category]: (report.incomeByCategory[txn.category] || 0) + txn.amount,\n },\n monthlyTotals: {\n ...report.monthlyTotals,\n [monthKey]: {\n income: (report.monthlyTotals[monthKey]?.income || 0) + txn.amount,\n expense: report.monthlyTotals[monthKey]?.expense || 0,\n },\n },\n };\n } else {\n return {\n ...report,\n totalExpense: report.totalExpense + txn.amount,\n netProfit: report.netProfit - txn.amount,\n expenseByCategory: {\n ...report.expenseByCategory,\n [txn.category]: (report.expenseByCategory[txn.category] || 0) + txn.amount,\n },\n monthlyTotals: {\n ...report.monthlyTotals,\n [monthKey]: {\n income: report.monthlyTotals[monthKey]?.income || 0,\n expense: (report.monthlyTotals[monthKey]?.expense || 0) + txn.amount,\n },\n },\n };\n }\n },\n {\n totalIncome: 0,\n totalExpense: 0,\n netProfit: 0,\n incomeByCategory: {},\n expenseByCategory: {},\n monthlyTotals: {},\n }\n );\n};\n\nconst report = generateReport(transactions);
Pythonでの実践例
from functools import reduce\nfrom typing import Dict, List, TypedDict\nfrom datetime import datetime\n\nclass Transaction(TypedDict):\n id: str\n amount: int\n type: str # 'income' or 'expense'\n category: str\n date: datetime\n\nclass MonthlyTotal(TypedDict):\n income: int\n expense: int\n\nclass FinancialReport(TypedDict):\n total_income: int\n total_expense: int\n net_profit: int\n income_by_category: Dict[str, int]\n expense_by_category: Dict[str, int]\n monthly_totals: Dict[str, MonthlyTotal]\n\ndef process_transaction(report: FinancialReport, txn: Transaction) -> FinancialReport:\n month_key = txn['date'].strftime('%Y-%m')\n \n if txn['type'] == 'income':\n income_cat = report['income_by_category']\n income_cat[txn['category']] = income_cat.get(txn['category'], 0) + txn['amount']\n \n if month_key not in report['monthly_totals']:\n report['monthly_totals'][month_key] = {'income': 0, 'expense': 0}\n \n report['monthly_totals'][month_key]['income'] += txn['amount']\n report['total_income'] += txn['amount']\n report['net_profit'] += txn['amount']\n else:\n expense_cat = report['expense_by_category']\n expense_cat[txn['category']] = expense_cat.get(txn['category'], 0) + txn['amount']\n \n if month_key not in report['monthly_totals']:\n report['monthly_totals'][month_key] = {'income': 0, 'expense': 0}\n \n report['monthly_totals'][month_key]['expense'] += txn['amount']\n report['total_expense'] += txn['amount']\n report['net_profit'] -= txn['amount']\n \n return report\n\ntransactions: List[Transaction] = [\n {'id': 'TXN001', 'amount': 500000, 'type': 'income', 'category': 'sales', 'date': datetime(2024, 1, 5)},\n {'id': 'TXN002', 'amount': 120000, 'type': 'expense', 'category': 'salary', 'date': datetime(2024, 1, 10)},\n {'id': 'TXN003', 'amount': 800000, 'type': 'income', 'category': 'services', 'date': datetime(2024, 1, 15)},\n]\n\ninitial_report: FinancialReport = {\n 'total_income': 0,\n 'total_expense': 0,\n 'net_profit': 0,\n 'income_by_category': {},\n 'expense_by_category': {},\n 'monthly_totals': {},\n}\n\nreport = reduce(process_transaction, transactions, initial_report)\nprint(report)
実装時の注意点と落とし穴
1. イミュータビリティの重要性
reduceで最も多い罪は元のaccumulatorを直接変更してしまうことです。これはバグの原因になります。
// ❌ 悪い例:直接変更
const badResult = data.reduce((acc, item) => {\n acc[item.id] = item; // オブジェクトを直接変更\n return acc;\n}, {});\n\n// ✅ 良い例:新しいオブジェクトを返す\nconst goodResult = data.reduce((acc, item) => ({\n ...acc,\n [item.id]: item, // スプレッド演算子で新規作成\n}), {});\n\n// ✅ 配列の場合\nconst arrayResult = data.reduce((acc, item) => [\n ...acc,\n item,\n], []);
2. パフォーマンスの考慮
複雑な変換や大規模データでは、reduceの効率性に注意が必要です。
// 10,000件のデータで複雑な処理\nconst largeData = Array.from({ length: 10000 }, (_, i) => ({\n id: i,\n value: Math.random() * 1000,\n}));\n\n// 注意:毎回スプレッド演算子でオブジェクトをコピーすると遅い\n// const bad = largeData.reduce((acc, item) => ({...acc, [item.id]: item}), {});\n\n// 改善:Object.assignを使用\nconst better = largeData.reduce((acc, item) => {\n acc[item.id] = item;\n return acc;\n}, {} as Record);\n\n// さらに改善:Mapを使用(キーの多い場合)\nconst best = largeData.reduce((acc, item) => {\n acc.set(item.id, item);\n return acc;\n}, new Map());
3. 初期値の設定ミス
初期値を設定しないと予期しない型エラーが発生します。
// ❌ エラーになりやすい\nconst result = [1, 2, 3].reduce((acc, val) => acc + val);\n// 初回: acc = 1, val = 2\n// 意図と異なる可能性がある\n\n// ✅ 明確に初期値を指定\nconst correctResult = [1, 2, 3].reduce((acc, val) => acc + val, 0);\n// 初回: acc = 0, val = 1\n\n// 空配列対策\nconst safeResult = emptyArray.reduce((acc, val) => acc + val, 0); // 0を返す
4. 複雑さのバランス
reduceが便利だからといって無理に詰め込むと、可読性が低下します。

