JavaScript flatMap の実践的な使い方|実務で役立つ5つのユースケース

JavaScript

JavaScript flatMap の実践的な使い方|実務で役立つ5つのユースケース

JavaScriptのflatMapメソッドは、配列操作の中でも特に業務システムやWebアプリケーション開発で頻繁に登場します。一見地味に思えますが、データ変換やAPI連携の場面で威力を発揮する重要な機能です。この記事では、単なる解説ではなく、実際のプロジェクトで使えるパターンを中心に説明していきます。

flatMap とは?簡潔な解説

flatMapは、配列の各要素に対して関数を実行し、その結果を1段階フラット化する組み込みメソッドです。言い換えると「mapしてからflatする」という2ステップを一度に行います。

// 基本的な動作
const numbers = [1, 2, 3];
const result = numbers.flatMap(n => [n, n * 2]);
console.log(result); // [1, 2, 2, 4, 3, 6]

多くの実務では、APIから取得したネストされたデータを平坦化しながら変換する、という処理が頻出です。

業務でよく使うユースケース

実務でflatMapが活躍する場面をいくつか紹介します。

ケース1:複数ユーザーの注文データを統合する

ECサイトの管理画面では、複数のユーザーデータから全ての注文を抽出して整形する必要があります。

const users = [
  {
    id: 1,
    name: \"田中太郎\",
    orders: [
      { orderId: \"ORD001\", amount: 5000, date: \"2024-01-15\" },
      { orderId: \"ORD002\", amount: 3200, date: \"2024-01-20\" }
    ]
  },
  {
    id: 2,
    name: \"佐藤花子\",
    orders: [
      { orderId: \"ORD003\", amount: 12000, date: \"2024-01-18\" }
    ]
  }
];

// 全ユーザーの注文を1つの配列に統合し、ユーザー情報も付与
const allOrders = users.flatMap(user =>
  user.orders.map(order => ({
    ...order,
    userName: user.name,
    userId: user.id
  }))
);

console.log(allOrders);
// [
//   { orderId: "ORD001", amount: 5000, date: "2024-01-15", userName: "田中太郎", userId: 1 },
//   { orderId: "ORD002", amount: 3200, date: "2024-01-20", userName: "田中太郎", userId: 1 },
//   { orderId: "ORD003", amount: 12000, date: "2024-01-18", userName: "佐藤花子", userId: 2 }
// ]

ポイントは、ユーザーごとのネスト構造をflatMapで展開しながら、同時に各注文にユーザー情報を追加している点です。

ケース2:複数の検索条件から該当者を抽出

ユーザーが複数の条件で検索した場合、各条件ごとのマッチ結果を統合します。

const employees = [
  { id: 101, name: \"山田太郎\", department: \"営業\", age: 35, skills: [\"営業\", \"Excel\"] },
  { id: 102, name: \"鈴木花子\", department: \"開発\", age: 28, skills: [\"JavaScript\", \"Python\"] },
  { id: 103, name: \"佐藤次郎\", department: \"営業\", age: 42, skills: [\"営業\", \"PowerPoint\"] },
  { id: 104, name: \"田中美咲\", department: \"開発\", age: 26, skills: [\"JavaScript\", \"React\"] }
];

const searchConditions = [
  { type: \"department\", value: \"営業\" },
  { type: \"skill\", value: \"JavaScript\" }
];

// 各検索条件にマッチした従業員を抽出(重複排除)
const matchedEmployees = Array.from(
  new Set(
    searchConditions.flatMap(condition => {
      if (condition.type === \"department\") {
        return employees.filter(emp => emp.department === condition.value);
      } else if (condition.type === \"skill\") {
        return employees.filter(emp => emp.skills.includes(condition.value));
      }
      return [];
    }).map(emp => emp.id)
  )
).map(id => employees.find(emp => emp.id === id));

console.log(matchedEmployees);
// 営業部門のユーザーとJavaScriptスキル保有者が統合される

ケース3:階層的なカテゴリデータをフラット化

商品管理システムで、カテゴリの階層構造をフラット化してセレクトボックスのオプションを生成します。

const categories = [
  {
    id: \"CAT001\",
    name: \"電子機器\",
    subCategories: [
      { id: \"SUB001\", name: \"スマートフォン\" },
      { id: \"SUB002\", name: \"ノートPC\" }
    ]
  },
  {
    id: \"CAT002\",
    name: \"衣類\",
    subCategories: [
      { id: \"SUB003\", name: \"メンズ\" },
      { id: \"SUB004\", name: \"レディース\" }
    ]
  }
];

// SELECTオプション用に階層化されたデータをフラット化
const selectOptions = categories.flatMap(cat =>
  cat.subCategories.map(sub => ({
    value: sub.id,
    label: `${cat.name} > ${sub.name}`,
    category: cat.id
  }))
);

console.log(selectOptions);
// [
//   { value: "SUB001", label: "電子機器 > スマートフォン", category: "CAT001" },
//   { value: "SUB002", label: "電子機器 > ノートPC", category: "CAT001" },
//   { value: "SUB003", label: "衣類 > メンズ", category: "CAT002" },
//   { value: "SUB004", label: "衣類 > レディース", category: "CAT002" }
// ]

TypeScript での型安全な実装

実務ではTypeScriptを使用するケースが増えています。flatMapを型安全に実装する方法を紹介します。

interface Order {
  orderId: string;
  amount: number;
  date: string;
}

interface User {
  id: number;
  name: string;
  orders: Order[];
}

interface FlattenedOrder extends Order {
  userName: string;
  userId: number;
}

const users: User[] = [
  {
    id: 1,
    name: \"鈴木太郎\",
    orders: [
      { orderId: \"ORD001\", amount: 8500, date: \"2024-01-10\" }
    ]
  }
];

const flatOrders: FlattenedOrder[] = users.flatMap(user =>
  user.orders.map(order => ({
    ...order,
    userName: user.name,
    userId: user.id
  }))
);

// 集計処理も型安全に
const totalAmount: number = flatOrders.reduce((sum, order) => sum + order.amount, 0);

API レスポンス処理での活用

実務で最も一般的なのはAPI連携です。複数のAPIエンドポイントから取得したデータを統合する場面です。

// 複数の店舗データからすべての商品を取得・統合
async function fetchAllProducts() {
  const stores = [
    { storeId: \"STORE001\", name: \"渋谷店\" },
    { storeId: \"STORE002\", name: \"新宿店\" }
  ];

  // 各店舗の商品APIを並列呼び出し
  const storeProductsPromises = stores.map(async store => {
    const response = await fetch(`/api/stores/${store.storeId}/products`);
    const products = await response.json();
    return products.map(product => ({
      ...product,
      storeName: store.name,
      storeId: store.storeId
    }));
  });

  const results = await Promise.all(storeProductsPromises);
  
  // 全店舗の商品を1つの配列に統合
  const allProducts = results.flatMap(products => products);
  
  return allProducts;
}

条件付きのflatMap処理

すべての要素を含めるのではなく、条件によって要素数を変える必要がある場合もあります。

const reports = [
  { reportId: \"REP001\", status: \"approved\", items: [{ itemId: \"I001\", value: 100 }, { itemId: \"I002\", value: 200 }] },
  { reportId: \"REP002\", status: \"draft\", items: [{ itemId: \"I003\", value: 150 }] },
  { reportId: \"REP003\", status: \"approved\", items: [{ itemId: \"I004\", value: 300 }] }
];

// 承認済みレポートのアイテムだけを抽出
const approvedItems = reports.flatMap(report =>
  report.status === \"approved\" ? report.items : []
);

console.log(approvedItems);
// [
//   { itemId: "I001", value: 100 },
//   { itemId: "I002", value: 200 },
//   { itemId: "I004", value: 300 }
// ]

Python での同等実装

参考までに、Pythonで同じ処理を実装した場合を示します。

from typing import List, Dict, Any

users = [
    {
        \"id\": 1,
        \"name\": \"田中太郎\",
        \"orders\": [
            {\"orderId\": \"ORD001\", \"amount\": 5000, \"date\": \"2024-01-15\"},
            {\"orderId\": \"ORD002\", \"amount\": 3200, \"date\": \"2024-01-20\"}
        ]
    },
    {
        \"id\": 2,
        \"name\": \"佐藤花子\",
        \"orders\": [
            {\"orderId\": \"ORD003\", \"amount\": 12000, \"date\": \"2024-01-18\"}
        ]
    }
]

# JavaScriptのflatMapに相当する処理
def flatten_orders(users: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
    return [
        {**order, \"userName\": user[\"name\"], \"userId\": user[\"id\"]}
        for user in users
        for order in user[\"orders\"]
    ]

all_orders = flatten_orders(users)
print(all_orders)

# list comprehensionを使わずにmap/filterで書く場合
from itertools import chain

all_orders_alt = list(chain.from_iterable(
    map(
        lambda order: {**order, \"userName\": user[\"name\"], \"userId\": user[\"id\"]},
        user[\"orders\"]
    )
    for user in users
))

よくある応用パターン

パターン1:グループ化と統計処理の組み合わせ

const salesData = [
  { department: \"営業\", monthlyRevenue: [100000, 150000, 120000] },
  { department: \"企画\", monthlyRevenue: [80000, 90000, 85000] }
];

// 月ごとの収益データをフラット化し、統計を取る
const monthlyStats = salesData.flatMap(dept =>
  dept.monthlyRevenue.map((revenue, index) => ({
    department: dept.department,
    month: index + 1,
    revenue: revenue
  }))
);

const departmentTotals = monthlyStats.reduce((acc, stat) => {
  if (!acc[stat.department]) acc[stat.department] = 0;
  acc[stat.department] += stat.revenue;
  return acc;
}, {});

console.log(departmentTotals);
// { 営業: 370000, 企画: 255000 }

パターン2:動的に要素数を変えるflatMap

const tasks = [
  { taskId: \"T001\", priority: \"high\", subtasks: [\"s1\", \"s2\", \"s3\"] },
  { taskId: \"T002\", priority: \"low\", subtasks: [\"s4\"] },
  { taskId: \"T003\", priority: \"high\", subtasks: [\"s5\", \"s6\"] }
];

// 高優先度タスクのサブタスクだけを処理
const highPrioritySubtasks = tasks.flatMap(task =>
  task.priority === \"high\" 
    ? task.subtasks.map(st => ({ taskId: task.taskId, subtask: st }))
    : []
);

console.log(highPrioritySubtasks);
// [
//   { taskId: "T001", subtask: "s1" },
//   { taskId: "T001", subtask: "s2" },
//   { taskId: "T001", subtask: "s3" },
//   { taskId: "T003", subtask: "s5" },
//   { taskId: "T003", subtask: "s6" }
// ]

パターン3:複数行のCSVデータを解析

const csvData = `name,tags
田中太郎,営業;マネジメント;企画
鈴木花子,開発;JavaScript;React
佐藤次郎,デザイン;UI/UX`;

const records = csvData.trim().split('\\n').slice(1).map(line => {
  const [name, tagsStr] = line.split(',');
  return { name, tags: tagsStr.split(';') };
});

// 各人物のタグを個別レコードに変換
const tagRecords = records.flatMap(record =>
  record.tags.map(tag => ({
    name: record.name,
    tag: tag
  }))
);

console.log(tagRecords);
// [
//   { name: "田中太郎", tag: "営業" },
//   { name: "田中太郎", tag: "マネジメント" },
//   { name: "田中太郎", tag: "企画" },
//   ...
// ]

注意点と落とし穴

注意1:パフォーマンスを意識する

大規模データセットでflatMapを使う場合、メモリ使用量に注意が必要です。

// ❌ 危険:大規模配列で複数回のflatMapを連鎖させる
const result = largeArray
  .flatMap(item => item.values)
  .flatMap(value => value.details)
  .flatMap(detail => detail.nested);

// ✅ 改善:1度の処理で完結させる
const result = largeArray.flatMap(item =>
  item.values.flatMap(value =>
    value.details.flatMap(detail =>
      detail.nested
    )
  )
);

注意2:undefined や null への対応

const data = [
  { id: 1, items: [1, 2, 3] },
  { id: 2, items: null },  // nullが混在
  { id: 3, items: [4, 5] }
];

// ❌ エラーになる可能性
const result = data.flatMap(d => d.items);

// ✅ 安全な実装
const result = data.flatMap(d => d.items || []);

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

注意3:深いネスト構造への対応

flatMapは1段階のみフラット化します。深いネスト構造にはflat(depth)との組み合わせが必要です。

const deepNested = [
  { id: 1, data: [[1, 2], [3, 4]] },
  { id: 2, data: [[5, 6]] }
];

// flatMapだけでは1段階のみ
const shallow = deepNested.flatMap(item => item.data);
console.log(shallow); // [[1, 2], [3, 4], [5, 6]]

// 完全にフラット化したい場合
const flat = deepNested.flatMap(item => item.data).flat();
console.log(flat); // [1, 2, 3, 4, 5, 6]

注意4:ブラウザ互換性

flatMapはES2019の機能です。古いブラウザをサポートする必要がある場合はpolyfillが必要です。実務ではBabelなどのトランスパイラで自動変換されることが多いですが、念頭に置いておきましょう。

デバッグのコツ

flatMapの処理が複雑な場合、段階的に検証することが重要です。

const data = [
  { id: 1, values: [10, 20] },
  { id: 2, values: [30] }
];

// ステップバイステップで確認
console.log(\"元データ:\", data);

const step1 = data.map(item => ({
  ...item,
  processedValues: item.values.map(v => v * 2)
}));
console.log(\"Step1 (map):\", step1);

const result = data.flatMap(item =>
  item.values.map(value => ({
    id: item.id,
    originalValue: value,
    doubledValue: value * 2
  }))
);
console.log(\"最終結果:\", result);

実務での推奨パターン

業務システムではflatMapを単独で使うより、他の配列メソッドと組み合わせることが多いです。

// 完全な実務例:注文データの集計
const orders = [
  {
    orderId: \"ORD001\",
    status: \"completed\",
    items: [
      { productId: \"P001\", quantity: 2, price: 5000 },
      { productId: \"P002\", quantity: 1, price: 3000 }
    ]
  },
  {
    orderId: \"ORD002\",
    status: \"cancelled\",
    items: [
      { productId: \"P003\", quantity: 3, price: 2000 }
    ]
  },
  {
    orderId: \"ORD003\",
    status: \"completed\",
    items: [
      { productId: \"P001\", quantity: 1, price: 5000 }
    ]
  }
];

// 完了した注文のアイテムを抽出し、売上を集計
const completedSales = orders
  .filter(order => order.status === \"completed\")
  .flatMap(order =>
    order.items.map(item => ({
      ...item,
      orderId: order.orderId,
      revenue: item.quantity * item.price
    }))
  );

// 商品ごとの売上集計
const productRevenue = completedSales.reduce((acc, sale) => {
  if (!acc[sale.productId]) {
    acc[sale.productId] = 0;
  }
  acc[sale.productId] += sale.revenue;
  return acc;
}, {});

console.log(\"商品別売上:\", productRevenue);
// { P001: 15000, P002: 3000 }

// 総売上
const totalRevenue = completedSales.reduce((sum, sale) => sum + sale.revenue, 0);
console.log(\"総売上:\", totalRevenue); // 18000

まとめ

JavaScriptのflatMapは、単純に見えて非常に強力なメソッドです。実務では以下のような場面で活躍します:

  • ネストされたデータの展開:複数ユーザーの注文やタスクを1つの配列に統合
  • API連携:複数のエンドポイントから取得したデータを統合処理
  • 階層構造のフラット化:カテゴリやツリー構造をセレクトボックスのオプションに変換
  • 条件付き変換:特定の条件を満たす要素だけを抽出・変換
  • データ集計:複雑な変換と統計処理の組み合わせ

flatMapを使うことで、従来はforループやreduceで記述していた複雑な処理を簡潔に書けます。ただし、型安全性やエラーハンドリングには注意が必要です。TypeScriptを使用する際は、インターフェース定義を明確にし、null/undefinedへの対応を忘れないようにしましょう。

実務では、パフォーマンス面の考慮も重要です。大規模データセットを処理する場合は、flatMapの連鎖を避け、可能な限り単一の処理で完結させることをお勧めします。

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