JavaScript reduce を実務で活用する完全ガイド|実践的なサンプルコード集
1. reduce メソッドの基礎知識
JavaScriptの reduce() メソッドは、配列の各要素に対して順序立てて関数を実行し、単一の値に集約するメソッドです。教科書的には「累積値」と「現在値」を受け取る関数を引数に取ります。
基本構文は以下の通りです:
array.reduce((accumulator, currentValue, index, array) => {
// 累積処理
return accumulator;
}, initialValue);
しかし実務では、単純な集約に留まらず、複雑なデータ変換やフィルタリング、グループ化といった処理に活躍します。本記事では教科書的なサンプルではなく、実際のプロジェクトで使用するコードパターンを紹介します。
2. 実務で頻出する reduce のユースケース
2-1. API レスポンスの集計・変換
実務で最も多いのは、バックエンドから取得したAPIレスポンスを加工する場面です。例えば、複数の売上レコードから日別・商品別の集計を行う必要があります。
// ユースケース:売上データの日別集計
interface SalesRecord {
id: number;
date: string; // YYYY-MM-DD
product: string;
amount: number;
quantity: number;
}
const salesData: SalesRecord[] = [
{ id: 1, date: '2024-01-15', product: 'リンゴ', amount: 1500, quantity: 3 },
{ id: 2, date: '2024-01-15', product: 'オレンジ', amount: 2000, quantity: 2 },
{ id: 3, date: '2024-01-16', product: 'リンゴ', amount: 1500, quantity: 3 },
{ id: 4, date: '2024-01-16', product: 'バナナ', amount: 800, quantity: 1 },
];
// 日別の売上合計を集計
const dailySalesTotal = salesData.reduce<Record<string, number>>(
(acc, record) => {
if (!acc[record.date]) {
acc[record.date] = 0;
}
acc[record.date] += record.amount;
return acc;
},
{}
);
console.log(dailySalesTotal);
// { '2024-01-15': 3500, '2024-01-16': 2300 }
2-2. ネストされたデータ構造の平坦化
複雑に入れ子になったオブジェクトや配列を扱う際、 reduce で平坦化処理を行うのは効率的です。
// ユースケース:注文データのフラット化
interface Order {
orderId: number;
customerId: number;
items: {
productId: number;
name: string;
price: number;
quantity: number;
}[];
}
const orders: Order[] = [
{
orderId: 1001,
customerId: 100,
items: [
{ productId: 1, name: 'ノートPC', price: 120000, quantity: 1 },
{ productId: 2, name: 'マウス', price: 3000, quantity: 2 },
],
},
{
orderId: 1002,
customerId: 101,
items: [
{ productId: 3, name: 'キーボード', price: 8000, quantity: 1 },
],
},
];
interface FlattenedItem {
orderId: number;
customerId: number;
productId: number;
name: string;
price: number;
quantity: number;
}
// すべてのアイテムを平坦化
const allItems = orders.reduce<FlattenedItem[]>(
(acc, order) => {
const flatItems = order.items.map((item) => ({
orderId: order.orderId,
customerId: order.customerId,
...item,
}));
return [...acc, ...flatItems];
},
[]
);
console.log(allItems);
// 3 つのアイテムが平坦化された配列
2-3. グループ化処理
データを特定のキーでグループ分けする処理は、テーブル表示やレポート生成で頻出です。
// ユースケース:従業員データの部門別グループ化
interface Employee {
id: number;
name: string;
department: string;
salary: number;
}
const employees: Employee[] = [
{ id: 1, name: '田中太郎', department: '営業', salary: 4500000 },
{ id: 2, name: '鈴木花子', department: 'エンジニア', salary: 5500000 },
{ id: 3, name: '佐藤次郎', department: '営業', salary: 4200000 },
{ id: 4, name: '山田美咲', department: 'エンジニア', salary: 6000000 },
{ id: 5, name: '高橋健太', department: 'HR', salary: 4000000 },
];
type GroupedEmployees = Record;
const employeesByDept = employees.reduce<GroupedEmployees>(
(acc, employee) => {
if (!acc[employee.department]) {
acc[employee.department] = [];
}
acc[employee.department].push(employee);
return acc;
},
{}
);
console.log(employeesByDept);
// {
// 営業: [{ id: 1, name: '田中太郎', ... }, { id: 3, name: '佐藤次郎', ... }],
// エンジニア: [{ id: 2, name: '鈴木花子', ... }, { id: 4, name: '山田美咲', ... }],
// HR: [{ id: 5, name: '高橋健太', ... }]
// }
3. 実装コード:複雑なビジネスロジックの例
3-1. 多段階の集計と計算
実務では単一の集計では足りず、複数の指標を同時に計算する必要があります。以下は、売上データから複数の統計情報を一度に算出する例です。
// ユースケース:売上分析ダッシュボード用のデータ準備
interface SalesAnalysis {
totalRevenue: number;
totalQuantity: number;
averageOrderValue: number;
orderCount: number;
topProduct: string;
productStats: Record<string, { revenue: number; quantity: number; orders: number }>;
}
const analyzesSales = (sales: SalesRecord[]): SalesAnalysis => {
const analysis = sales.reduce(
(acc, record) => {
// 基本集計
acc.totalRevenue += record.amount;
acc.totalQuantity += record.quantity;
acc.orderCount += 1;
// 商品別統計
if (!acc.productStats[record.product]) {
acc.productStats[record.product] = {
revenue: 0,
quantity: 0,
orders: 0,
};
}
acc.productStats[record.product].revenue += record.amount;
acc.productStats[record.product].quantity += record.quantity;
acc.productStats[record.product].orders += 1;
// トップ商品の追跡
if (
acc.productStats[record.product].revenue >
acc.productStats[acc.topProduct].revenue
) {
acc.topProduct = record.product;
}
return acc;
},
{
totalRevenue: 0,
totalQuantity: 0,
orderCount: 0,
topProduct: '',
productStats: {} as Record<string, { revenue: number; quantity: number; orders: number }>,
}
);
return {
...analysis,
averageOrderValue: analysis.totalRevenue / analysis.orderCount,
};
};
const result = analyzesSales(salesData);
console.log(result);
3-2. 条件付きフィルタリングと変換
reduce を使うことで、フィルタリングと変換を同時に行えます。map + filter + map のチェーンよりも効率的です。
// ユースケース:高額注文のみをピックアップして集計
interface ProcessedOrder {
orderId: number;
totalAmount: number;
itemCount: number;
status: string;
}
const MIN_ORDER_AMOUNT = 50000;
const processHighValueOrders = (orders: Order[]): ProcessedOrder[] => {
return orders.reduce<ProcessedOrder[]>((acc, order) => {
const totalAmount = order.items.reduce(
(sum, item) => sum + item.price * item.quantity,
0
);
// 条件チェック:MIN_ORDER_AMOUNT 以上のみ
if (totalAmount >= MIN_ORDER_AMOUNT) {
acc.push({
orderId: order.orderId,
totalAmount,
itemCount: order.items.length,
status: totalAmount > 100000 ? 'VIP' : 'STANDARD',
});
}
return acc;
}, []);
};
const highValueOrders = processHighValueOrders(orders);
console.log(highValueOrders);
3-3. キーバリュー形式への変換
配列をキーバリュー形式に変換する処理も reduce で効率的に行えます。
// ユースケース:ユーザーIDをキーとしたマップの構築
interface User {
id: number;
name: string;
email: string;
role: string;
}
const users: User[] = [
{ id: 1, name: '太郎', email: 'taro@example.com', role: 'admin' },
{ id: 2, name: '花子', email: 'hanako@example.com', role: 'user' },
{ id: 3, name: '次郎', email: 'jiro@example.com', role: 'user' },
];
type UserMap = Record<number, User>;
const userMap = users.reduce<UserMap>((acc, user) => {
acc[user.id] = user;
return acc;
}, {});
console.log(userMap[1]); // { id: 1, name: '太郎', ... }
4. よくある応用パターン
4-1. reduce + map の組み合わせ
配列内の各要素がさらに配列を持つ場合、 reduce 内で map を使うことで柔軟に対応できます。
// ユースケース:複数のカテゴリー内の商品を統合・変換
interface Category {
categoryId: number;
name: string;
products: {
productId: number;
name: string;
price: number;
}[];
}
const categories: Category[] = [
{
categoryId: 1,
name: '家電',
products: [
{ productId: 101, name: 'TV', price: 50000 },
{ productId: 102, name: '冷蔵庫', price: 80000 },
],
},
{
categoryId: 2,
name: '家具',
products: [
{ productId: 201, name: 'ソファ', price: 120000 },
],
},
];
interface EnrichedProduct {
productId: number;
name: string;
price: number;
categoryName: string;
displayName: string;
}
const getAllProductsEnriched = (categories: Category[]): EnrichedProduct[] => {
return categories.reduce<EnrichedProduct[]>((acc, category) => {
const enriched = category.products.map((product) => ({
productId: product.productId,
name: product.name,
price: product.price,
categoryName: category.name,
displayName: `[${category.name}] ${product.name}`,
}));
return [...acc, ...enriched];
}, []);
};
const allProducts = getAllProductsEnriched(categories);
console.log(allProducts);
4-2. 複数の累積値の並行処理
複数の集計値を同時に計算する必要がある場合、単一のオブジェクトに複数のプロパティを持たせます。
// ユースケース:複数の統計情報を一度に算出
interface Statistics {
sum: number;
count: number;
min: number;
max: number;
average: number;
}
const numbers = [15, 32, 48, 12, 67, 23, 89, 41];
const calculateStats = (nums: number[]): Statistics => {
const stats = nums.reduce(
(acc, num) => {
acc.sum += num;
acc.count += 1;
acc.min = Math.min(acc.min, num);
acc.max = Math.max(acc.max, num);
return acc;
},
{
sum: 0,
count: 0,
min: Infinity,
max: -Infinity,
}
);
return {
...stats,
average: stats.sum / stats.count,
};
};
const stats = calculateStats(numbers);
console.log(stats);
// { sum: 327, count: 8, min: 12, max: 89, average: 40.875 }
4-3. 条件分岐による複数のグループ化
複数の条件に基づいて同時にグループ分けする場合も reduce で対応できます。
// ユースケース:売上を複数の軸(地域・期間)でグループ化
interface RegionalSales {
date: string;
region: string;
amount: number;
}
const regionalSales: RegionalSales[] = [
{ date: '2024-01', region: '北海道', amount: 150000 },
{ date: '2024-01', region: '関東', amount: 500000 },
{ date: '2024-02', region: '北海道', amount: 120000 },
{ date: '2024-02', region: '関東', amount: 450000 },
{ date: '2024-02', region: '関西', amount: 300000 },
];
type MultiDimensionalGroup = Record<
string,
Record<string, number>
>
const groupByDateAndRegion = (
sales: RegionalSales[]
): MultiDimensionalGroup => {
return sales.reduce<MultiDimensionalGroup>((acc, sale) => {
if (!acc[sale.date]) {
acc[sale.date] = {};
}
acc[sale.date][sale.region] = sale.amount;
return acc;
}, {});
};
const grouped = groupByDateAndRegion(regionalSales);
console.log(grouped);
// {
// '2024-01': { '北海道': 150000, '関東': 500000 },
// '2024-02': { '北海道': 120000, '関東': 450000, '関西': 300000 }
// }
5. reduce 使用時の注意点と落とし穴
5-1. オブジェクトのミューテーション
累積値がオブジェクトの場合、誤ってオブジェクトを直接変更してしまう危険があります。
// 危険なパターン:元のオブジェクトが変更される
const dangerousPattern = (records: SalesRecord[]) => {
const baseData = { total: 0, items: [] };
return records.reduce((acc, record) => {
// 間違い:baseData が直接変更される
acc.total += record.amount;
acc.items.push(record.product);
return acc;
}, baseData);
};
// 安全なパターン:新しいオブジェクトを返す
const safePattern = (records: SalesRecord[]) => {
return records.reduce(
(acc, record) => ({
...acc,
total: acc.total + record.amount,
items: [...acc.items, record.product],
}),
{ total: 0, items: [] }
);
};
5-2. 初期値の型指定
TypeScript を使う場合、初期値の型を明示することで予期しないエラーを防げます。
// 間違い:型推論が正しく行われない
const wrongType = salesData.reduce((acc, record) => {
// acc の型が正確に推論されない
acc[record.date] = (acc[record.date] || 0) + record.amount;
return acc;
}, {});
// 正しい:型を明示
const correctType = salesData.reduce<Record<string, number>>(
(acc, record) => {
acc[record.date] = (acc[record.date] || 0) + record.amount;
return acc;
},
{}
);
5-3. 大規模データセットのパフォーマンス
reduce は便利ですが、複雑な処理ではメモリ効率が悪くなる可能性があります。大規模データの場合は、ジェネレータやストリーム処理の検討も必要です。
// 大規模データセット向けの最適化パターン
const processLargeDataset = (records: SalesRecord[]) => {
const BATCH_SIZE = 1000;
const results: SalesRecord[] = [];
for (let i = 0; i < records.length; i += BATCH_SIZE) {
const batch = records.slice(i, i + BATCH_SIZE);
const batchResult = batch.reduce<SalesRecord[]>(
(acc, record) => {
if (record.amount > 1000) {
acc.push(record);
}
return acc;
},
[]
);
results.push(...batchResult);
}
return results;
};
5-4. 空配列への対応
入力配列が空の場合、初期値が返されます。初期値は適切に設定する必要があります。
// 初期値なしの場合、空配列ではエラーになる
try {
const empty: number[] = [];
const sum = empty.reduce((acc, val) => acc + val); // TypeError
} catch (e) {
console.error('エラー:空配列に初期値なし');
}
// 初期値ありなら安全
const sum = [].reduce((acc, val) => acc + val, 0); // 0
const grouped = [].reduce<Record<string, any[]>>(
(acc, val) => acc,
{}
); // {}
5-5. チェーン可能性との兼ね合い
reduce の後にさらに配列メソッドを実行する場合、最初から別の方法を検討したほうが読みやすい場合があります。
// reduce で配列を返して、さらに map をする場合
const chained = salesData
.reduce<ProcessedOrder[]>((acc, record) => {
// 処理...
return acc;
}, [])
.map((item) => item.totalAmount);
// 最初から filter + map を使ったほうが読みやすい場合もある
const alternative = salesData
.filter((record) => record.amount > 1000)
.map((record) => ({
...record,
category: record.amount > 5000 ? 'high' : 'low',
}));
6. パフォーマンスと可読性のバランス
reduce は強力なツールですが、過度に複雑な処理を reduce 内に詰め込むと、可読性が低下します。以下のような場合は、複数のステップに分けることをお勧めします。
// 複雑すぎるパターン:避けるべき
const complexReduce = salesData.reduce((acc, record) => {
const key = `${record.date}_${record.product}`;
if (!acc[key]) {
acc[key] = { count: 0, total: 0, avg: 0 };
}
acc[key].count++;
acc[key].total += record.amount;
acc[key].avg = acc[key].total / acc[key].count;
return acc;
}, {});
// 読みやすいパターン:ヘルパー関数に分ける
const createKey = (record: SalesRecord) =>
`${record.date}_${record.product}`;
const updateStats = (
stats: Record<string, any>,
key: string,
amount: number
) => {
if (!stats[key]) {
stats[key] = { count: 0, total: 0 };
}
stats[key].count++;
stats[key].total += amount;
stats[key].avg = stats[key].total / stats[key].count;
return stats;
};
const readableReduce = salesData.reduce(
(acc, record) =>
updateStats(acc, createKey(record), record.amount),
{}
);
7. Python での reduce の使用例
JavaScriptの知見を Python でも活用できます。Python では functools.reduce を使います。
from functools import reduce
from typing import List, Dict, TypedDict
class SalesRecord(TypedDict):
date: str
product: str
amount: int
# TypeScript の例と同じく、日別売上を集計
sales_data: List[SalesRecord] = [
{'date': '2024-01-15', 'product': 'リンゴ', 'amount': 1500},
{'date': '2024-01-15', 'product': 'オレンジ', 'amount': 2000},
{'date': '2024-01-16', 'product': 'リンゴ', 'amount': 1500},
]
def aggregate_daily_sales(sales: List[SalesRecord]) -> Dict[str, int]:
def reducer(acc: Dict[str, int], record: SalesRecord) -> Dict[str, int]:
acc[record['date']] = acc.get(record['date'], 0) + record['amount']
return acc
return reduce(reducer, sales, {})
result = aggregate_daily_sales(sales_data)
print(result)
# {'2024-01-15': 3500, '2024-01-16': 1500}
# グループ化の例
def group_by_product(sales: List[SalesRecord]) -> Dict[str, List[int]]:
def reducer(acc: Dict[str, List[int]], record: SalesRecord) -> Dict[str, List[int]]:
if record['product'] not in acc:
acc[record['product']] = []
acc[record['product']].append(record['amount'])
return acc
return reduce(reducer, sales, {})
grouped = group_by_product(sales_data)
print(grouped)
# {'リンゴ': [1500, 1500], 'オレンジ': [2000]}
まとめ
JavaScript の reduce メソッドは、データ集計、グループ化、変換といった実務的なタスクで非常に有用です。本記事で紹介したパターンは、以下のような場面で活躍します:
- API レスポンスの複雑な加工
- 複数軸でのデータグループ化
- 複数の統計情報の並行計算
- 配列から辞書形式への変換
- 条件付きフィルタリングと変換の同時実行
ただし、過度に複雑な処理を reduce に詰め込むと可読性が低下するため、ヘルパー関数を活用したり、別のアプローチを検討したりすることも重要です。TypeScript を使う場合は型指定を明確にすることで、バグを事前に防ぐことができます。
reduce をマスターすることで、JavaScriptでのデータ処理の効率と品質が大きく向上します。

