JavaScript reduce を実務で活用する完全ガイド|実践的なサンプルコード集

JavaScript

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でのデータ処理の効率と品質が大きく向上します。

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