T-CREATOR

Lodash クイックレシピ :配列・オブジェクト変換の“定番ひな形”集

Lodash クイックレシピ :配列・オブジェクト変換の“定番ひな形”集

JavaScript 開発において、配列やオブジェクトの操作は日常的な作業です。しかし、複雑なデータ変換や処理を効率的に行うには、適切なツールの知識が欠かせません。

Lodash は、JavaScript 開発者にとって最も信頼されているユーティリティライブラリの一つです。配列とオブジェクトの操作を簡潔で読みやすいコードで実現でき、開発効率を大幅に向上させます。

本記事では、実務でよく使われる 50 の Lodash レシピを機能別に整理し、コピー&ペーストで使える"定番ひな形"として紹介します。初心者の方でも理解しやすいよう、各レシピには具体的なコード例と詳しい解説を付けました。

Lodash 関数早見表

実際の作業でよく使用するLodash関数を目的別に整理した早見表です。各関数の詳細な使用例は、対応するセクションをご参照ください。

#関数名分類用途よく使うパターン
1_.fill()配列生成同じ値で配列を初期化_.fill(Array(5), 0)
2_.times()配列生成指定回数の関数実行で配列作成_.times(3, () => ({ id: i }))
3_.range()配列生成連続した数値配列の作成_.range(1, 10, 2)
4_.random()配列生成ランダム値の生成_.random(1, 100)
5_.map()配列変換配列の各要素を変換_.map(users, 'name')
6_.flatten()配列変換1レベル平坦化_.flatten([1, [2, 3]])
7_.flattenDeep()配列変換完全平坦化_.flattenDeep([1, [2, [3]]])
8_.chunk()配列変換配列をチャンクに分割_.chunk([1,2,3,4], 2)
9_.find()配列検索条件に一致する最初の要素_.find(users, { active: true })
10_.findIndex()配列検索条件に一致する要素のインデックス_.findIndex(users, { name: 'Alice' })
11_.filter()配列検索条件に一致する要素をすべて取得_.filter(products, p => p.price > 100)
12_.uniq()配列検索重複除去_.uniq([1, 2, 2, 3])
13_.uniqBy()配列検索条件による重複除去_.uniqBy(users, 'id')
14_.take()配列検索先頭から指定数の要素取得_.take(numbers, 3)
15_.takeRight()配列検索末尾から指定数の要素取得_.takeRight(numbers, 3)
16_.sum()配列集計数値配列の合計_.sum([1, 2, 3, 4])
17_.sumBy()配列集計プロパティ値の合計_.sumBy(orders, 'amount')
18_.max()配列集計最大値取得_.max([1, 3, 2])
19_.maxBy()配列集計条件による最大値取得_.maxBy(users, 'age')
20_.groupBy()配列集計配列のグループ化_.groupBy(students, 'grade')
21_.countBy()配列集計要素のカウント_.countBy(votes)
22_.keyBy()配列→オブジェクト配列をキー・値のオブジェクトに変換_.keyBy(users, 'id')
23_.zipObject()配列→オブジェクトキーと値の配列からオブジェクト作成_.zipObject(keys, values)
24_.keys()オブジェクト→配列オブジェクトのキー配列取得_.keys(userStats)
25_.values()オブジェクト→配列オブジェクトの値配列取得_.values(userStats)
26_.toPairs()オブジェクト→配列キー・値ペアの配列に変換_.toPairs(obj)
27_.get()オブジェクト操作安全なプロパティ取得_.get(user, 'address.city', 'N​/​A')
28_.set()オブジェクト操作ネストしたプロパティの設定_.set(obj, 'user.name', 'Alice')
29_.has()オブジェクト操作プロパティ存在チェック_.has(obj, 'user.profile')
30_.pick()オブジェクト操作特定プロパティのみ選択_.pick(user, ['id', 'name'])
31_.omit()オブジェクト操作特定プロパティを除外_.omit(user, ['password'])
32_.mapKeys()オブジェクト変換キー名の変換_.mapKeys(obj, (v, k) => _.camelCase(k))
33_.mapValues()オブジェクト変換値の変換_.mapValues(obj, v => v.toUpperCase())
34_.assign()オブジェクト結合浅いマージ_.assign({}, obj1, obj2)
35_.merge()オブジェクト結合深いマージ_.merge({}, defaults, userConfig)
36_.defaultsDeep()オブジェクト結合デフォルト値との深いマージ_.defaultsDeep({}, user, defaults)
37_.cloneDeep()オブジェクト複製深いコピー_.cloneDeep(complexObject)
38_.isArray()型チェック配列かどうかの判定_.isArray(value)
39_.isEmpty()型チェック空の値かどうかの判定_.isEmpty(obj)

この早見表を参考に、目的に応じて適切なLodash関数を選択してください。

配列操作の基本レシピ

配列の操作は、フロントエンド開発からバックエンド処理まで、あらゆる場面で必要になります。Lodash を使うことで、ネイティブの JavaScript よりも直感的で安全な配列操作が可能になります。

配列の生成・初期化

配列を生成する際の基本的なパターンをご紹介します。特定の値で初期化したり、連続した数値を作成したりする処理は、データの準備段階でよく使われます。

1. 指定した値で配列を初期化する

javascriptconst _ = require('lodash');

// 同じ値で配列を初期化
const zeros = _.fill(Array(5), 0);
console.log(zeros); // [0, 0, 0, 0, 0]

// オブジェクトで初期化(参照を分離)
const users = _.times(3, () => ({ name: '', age: 0 }));
console.log(users);
// [{ name: '', age: 0 }, { name: '', age: 0 }, { name: '', age: 0 }]

_.fill() は配列を指定した値で埋める関数で、初期化処理に便利です。_.times() は指定回数だけ関数を実行し、その結果で配列を作成します。

2. 連続した数値の配列を作成する

javascript// 0から9までの配列
const range1 = _.range(10);
console.log(range1); // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

// 1から5までの配列
const range2 = _.range(1, 6);
console.log(range2); // [1, 2, 3, 4, 5]

// ステップを指定した配列
const range3 = _.range(0, 21, 5);
console.log(range3); // [0, 5, 10, 15, 20]

_.range() 関数は開始値、終了値、ステップを指定して連続した数値配列を簡単に作成できます。

3. ランダムな値で配列を生成する

javascript// ランダムな整数で配列を作成
const randomInts = _.times(5, () => _.random(1, 100));
console.log(randomInts); // [42, 8, 73, 15, 91] (実行ごとに変わる)

// ランダムな文字列で配列を作成
const randomStrings = _.times(3, () =>
  _.sampleSize('abcdefghijklmnopqrstuvwxyz', 5).join('')
);
console.log(randomStrings); // ['djkwp', 'mzqrt', 'xbvng'] (実行ごとに変わる)

テストデータの作成や、サンプルデータの準備でよく使われるパターンです。

配列の変換・加工

配列内の要素を変換したり、特定の条件で加工したりする処理は、データ処理の中核となる操作です。

4. 配列の各要素を変換する

javascriptconst numbers = [1, 2, 3, 4, 5];

// 各要素を2倍にする
const doubled = _.map(numbers, (n) => n * 2);
console.log(doubled); // [2, 4, 6, 8, 10]

// オブジェクト配列から特定のプロパティを抽出
const users = [
  { name: 'Alice', age: 25 },
  { name: 'Bob', age: 30 },
  { name: 'Charlie', age: 35 },
];

const names = _.map(users, 'name');
console.log(names); // ['Alice', 'Bob', 'Charlie']

_.map() は配列の各要素に関数を適用して新しい配列を作成します。プロパティ名を文字列で指定することで、オブジェクト配列から特定の値を抽出できます。

5. 配列を平坦化する

javascript// 1レベルの平坦化
const nested1 = [1, [2, 3], [4, [5, 6]]];
const flattened1 = _.flatten(nested1);
console.log(flattened1); // [1, 2, 3, 4, [5, 6]]

// 完全な平坦化
const nested2 = [1, [2, [3, [4, 5]]]];
const flattened2 = _.flattenDeep(nested2);
console.log(flattened2); // [1, 2, 3, 4, 5]

// 指定したレベルまで平坦化
const nested3 = [1, [2, [3, [4, 5]]]];
const flattened3 = _.flattenDepth(nested3, 2);
console.log(flattened3); // [1, 2, 3, [4, 5]]

ネストした配列構造を扱う際に、平坦化は重要な操作です。API レスポンスの加工などでよく使われます。

6. 配列をチャンクに分割する

javascriptconst array = [1, 2, 3, 4, 5, 6, 7, 8, 9];

// 3つずつのチャンクに分割
const chunks = _.chunk(array, 3);
console.log(chunks); // [[1, 2, 3], [4, 5, 6], [7, 8, 9]]

// 実際の用途例:ページネーション
const items = _.range(1, 21); // 1-20の配列
const pages = _.chunk(items, 5); // 5件ずつのページ
console.log(pages.length); // 4ページ
console.log(pages[0]); // [1, 2, 3, 4, 5]

大量のデータをページ単位で表示する際や、並列処理のためにデータを分割する際に使用します。

配列の検索・抽出

条件に基づいて配列から要素を検索したり抽出したりする操作は、データフィルタリングの基本となります。

7. 条件に一致する要素を検索する

javascriptconst users = [
  { id: 1, name: 'Alice', age: 25, active: true },
  { id: 2, name: 'Bob', age: 30, active: false },
  { id: 3, name: 'Charlie', age: 35, active: true },
];

// 最初に見つかった要素を取得
const activeUser = _.find(users, { active: true });
console.log(activeUser); // { id: 1, name: 'Alice', age: 25, active: true }

// 条件関数を使用した検索
const youngUser = _.find(users, (user) => user.age < 30);
console.log(youngUser); // { id: 1, name: 'Alice', age: 25, active: true }

// インデックスを取得
const bobIndex = _.findIndex(users, { name: 'Bob' });
console.log(bobIndex); // 1

_.find() は最初に条件に一致した要素を返し、_.findIndex() はそのインデックスを返します。

8. 条件に一致する要素をフィルタリングする

javascriptconst products = [
  { name: 'Laptop', price: 1200, category: 'Electronics' },
  { name: 'Book', price: 15, category: 'Education' },
  { name: 'Phone', price: 800, category: 'Electronics' },
  { name: 'Pen', price: 3, category: 'Office' },
];

// 価格が100以上の商品
const expensive = _.filter(
  products,
  (product) => product.price >= 100
);
console.log(expensive.length); // 2

// 特定のカテゴリの商品
const electronics = _.filter(products, {
  category: 'Electronics',
});
console.log(electronics.length); // 2

// 複数条件での絞り込み
const cheapElectronics = _.filter(
  products,
  (product) =>
    product.category === 'Electronics' &&
    product.price < 1000
);
console.log(cheapElectronics); // [{ name: 'Phone', price: 800, category: 'Electronics' }]

フィルタリングは検索機能や条件絞り込み機能の実装に欠かせない操作です。

9. 配列から重複を除去する

javascript// プリミティブ値の重複除去
const numbers = [1, 2, 2, 3, 3, 3, 4, 5];
const unique = _.uniq(numbers);
console.log(unique); // [1, 2, 3, 4, 5]

// オブジェクト配列の重複除去(特定のプロパティ基準)
const users = [
  { id: 1, name: 'Alice' },
  { id: 2, name: 'Bob' },
  { id: 1, name: 'Alice' },
  { id: 3, name: 'Charlie' },
];

const uniqueUsers = _.uniqBy(users, 'id');
console.log(uniqueUsers);
// [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }, { id: 3, name: 'Charlie' }]

// カスタム関数による重複除去
const uniqueByName = _.uniqBy(users, (user) =>
  user.name.toLowerCase()
);
console.log(uniqueByName.length); // 3

データクリーニングやマスターデータの管理で重要な機能です。

10. 配列の先頭・末尾から要素を取得する

javascriptconst numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

// 先頭から指定数の要素を取得
const first3 = _.take(numbers, 3);
console.log(first3); // [1, 2, 3]

// 末尾から指定数の要素を取得
const last3 = _.takeRight(numbers, 3);
console.log(last3); // [8, 9, 10]

// 条件を満たす間は要素を取得
const takeWhileSmall = _.takeWhile(numbers, (n) => n < 5);
console.log(takeWhileSmall); // [1, 2, 3, 4]

// 条件を満たす間は末尾から要素を取得
const takeRightWhileBig = _.takeRightWhile(
  numbers,
  (n) => n > 7
);
console.log(takeRightWhileBig); // [8, 9, 10]

ページネーションやランキング表示などで使用される機能です。

配列の集計・統計

配列内のデータを集計したり統計情報を取得したりする操作は、データ分析の基本となります。

11. 配列の要素を集計する

javascript// 数値配列の合計
const numbers = [1, 2, 3, 4, 5];
const sum = _.sum(numbers);
console.log(sum); // 15

// オブジェクト配列の特定プロパティの合計
const sales = [
  { product: 'A', amount: 100 },
  { product: 'B', amount: 200 },
  { product: 'C', amount: 150 },
];

const totalSales = _.sumBy(sales, 'amount');
console.log(totalSales); // 450

// 関数を使った集計
const totalSquared = _.sumBy(numbers, (n) => n * n);
console.log(totalSquared); // 55 (1 + 4 + 9 + 16 + 25)

売上集計や数値データの分析でよく使用されるパターンです。

12. 最大値・最小値を取得する

javascriptconst scores = [85, 92, 78, 96, 88];

// 最大値・最小値
const maxScore = _.max(scores);
const minScore = _.min(scores);
console.log(maxScore, minScore); // 96 78

// オブジェクト配列から最大・最小を取得
const players = [
  { name: 'Alice', score: 95 },
  { name: 'Bob', score: 87 },
  { name: 'Charlie', score: 92 },
];

const topPlayer = _.maxBy(players, 'score');
const bottomPlayer = _.minBy(players, 'score');
console.log(topPlayer.name); // Alice
console.log(bottomPlayer.name); // Bob

ランキング機能やパフォーマンス分析に使用される機能です。

13. 配列をグループ化する

javascriptconst students = [
  { name: 'Alice', grade: 'A', subject: 'Math' },
  { name: 'Bob', grade: 'B', subject: 'Math' },
  { name: 'Charlie', grade: 'A', subject: 'Science' },
  { name: 'David', grade: 'B', subject: 'Science' },
];

// 成績でグループ化
const byGrade = _.groupBy(students, 'grade');
console.log(byGrade);
// {
//   A: [{ name: 'Alice', grade: 'A', subject: 'Math' }, ...],
//   B: [{ name: 'Bob', grade: 'B', subject: 'Math' }, ...]
// }

// カスタム関数でグループ化
const bySubjectLength = _.groupBy(
  students,
  (student) => student.subject.length
);
console.log(Object.keys(bySubjectLength)); // ['4', '7'] (Math=4文字, Science=7文字)

// 複数条件でのグループ化
const byGradeAndSubject = _.groupBy(
  students,
  (student) => `${student.grade}-${student.subject}`
);
console.log(Object.keys(byGradeAndSubject)); // ['A-Math', 'B-Math', 'A-Science', 'B-Science']

データの分類や統計分析でよく使用されるパターンです。

14. 配列の要素をカウントする

javascriptconst votes = [
  'apple',
  'banana',
  'apple',
  'orange',
  'banana',
  'apple',
];

// 各要素の出現回数をカウント
const counts = _.countBy(votes);
console.log(counts); // { apple: 3, banana: 2, orange: 1 }

// オブジェクト配列のプロパティ値をカウント
const users = [
  { name: 'Alice', role: 'admin' },
  { name: 'Bob', role: 'user' },
  { name: 'Charlie', role: 'admin' },
  { name: 'David', role: 'user' },
];

const roleCounts = _.countBy(users, 'role');
console.log(roleCounts); // { admin: 2, user: 2 }

// 条件関数でカウント
const ageCounts = _.countBy(users, (user) =>
  user.name.length > 5 ? 'long' : 'short'
);
console.log(ageCounts); // { short: 3, long: 1 } (Charlie=7文字)

投票結果の集計やユーザー分析などに使用されます。

オブジェクト操作の基本レシピ

オブジェクトの操作は、設定データの管理、API レスポンスの加工、状態管理など、JavaScript 開発の多くの場面で必要になります。Lodash を使うことで、安全で効率的なオブジェクト操作が可能になります。

オブジェクトの生成・初期化

オブジェクトを動的に生成したり、既存のオブジェクトから新しいオブジェクトを作成したりする基本的なパターンをご紹介します。

15. キーと値の配列からオブジェクトを作成する

javascript// キーと値の配列からオブジェクト作成
const keys = ['name', 'age', 'city'];
const values = ['Alice', 25, 'Tokyo'];

const user = _.zipObject(keys, values);
console.log(user); // { name: 'Alice', age: 25, city: 'Tokyo' }

// ネストしたプロパティ名でオブジェクト作成
const nestedKeys = [
  'user.profile.name',
  'user.profile.age',
  'user.settings.theme',
];
const nestedValues = ['Bob', 30, 'dark'];

const nestedUser = {};
_.forEach(
  _.zip(nestedKeys, nestedValues),
  ([key, value]) => {
    _.set(nestedUser, key, value);
  }
);
console.log(nestedUser);
// { user: { profile: { name: 'Bob', age: 30 }, settings: { theme: 'dark' } } }

フォームデータの処理や設定オブジェクトの動的生成でよく使用されるパターンです。

16. 条件に基づいてオブジェクトを生成する

javascript// 条件に基づくプロパティの設定
const createUser = (name, age, isAdmin = false) => {
  const user = { name, age };

  // 管理者の場合のみ権限プロパティを追加
  if (isAdmin) {
    _.set(user, 'permissions', ['read', 'write', 'delete']);
    _.set(user, 'role', 'admin');
  }

  // 年齢に基づく分類
  _.set(user, 'category', age >= 18 ? 'adult' : 'minor');

  return user;
};

const admin = createUser('Alice', 25, true);
const regularUser = createUser('Bob', 17, false);

console.log(admin);
// { name: 'Alice', age: 25, permissions: ['read', 'write', 'delete'], role: 'admin', category: 'adult' }
console.log(regularUser);
// { name: 'Bob', age: 17, category: 'minor' }

ユーザー権限の管理や設定の動的生成に使用されます。

17. デフォルト値を持つオブジェクトを作成する

javascript// デフォルト設定オブジェクト
const defaultConfig = {
  theme: 'light',
  language: 'en',
  notifications: {
    email: true,
    push: false,
    sound: true,
  },
  timeout: 5000,
};

// ユーザー設定をマージして最終設定を作成
const userConfig1 = { theme: 'dark', language: 'ja' };
const userConfig2 = {
  notifications: { email: false },
  timeout: 10000,
};

const finalConfig1 = _.defaultsDeep(
  {},
  userConfig1,
  defaultConfig
);
const finalConfig2 = _.defaultsDeep(
  {},
  userConfig2,
  defaultConfig
);

console.log(finalConfig1.theme); // 'dark'
console.log(finalConfig1.notifications.sound); // true (デフォルト値)

console.log(finalConfig2.notifications.email); // false (ユーザー設定)
console.log(finalConfig2.notifications.push); // false (デフォルト値)

設定管理やオプションの初期化でよく使用されるパターンです。

オブジェクトの変換・加工

既存のオブジェクトを変換したり、特定の形式に加工したりする処理について説明します。

18. オブジェクトのプロパティを変換する

javascriptconst user = {
  firstName: 'Alice',
  lastName: 'Johnson',
  email: 'alice@example.com',
  age: 25,
  isActive: true,
};

// プロパティ名を変換
const renamedUser = _.mapKeys(user, (value, key) =>
  _.camelCase(key)
);
console.log(renamedUser); // 既にcamelCaseなので変化なし

// スネークケースに変換
const snakeCaseUser = _.mapKeys(user, (value, key) =>
  _.snakeCase(key)
);
console.log(snakeCaseUser);
// { first_name: 'Alice', last_name: 'Johnson', email: 'alice@example.com', age: 25, is_active: true }

// プロパティ値を変換
const transformedUser = _.mapValues(user, (value, key) => {
  if (typeof value === 'string') {
    return value.toUpperCase();
  }
  if (typeof value === 'boolean') {
    return value ? 'YES' : 'NO';
  }
  return value;
});
console.log(transformedUser);
// { firstName: 'ALICE', lastName: 'JOHNSON', email: 'ALICE@EXAMPLE.COM', age: 25, isActive: 'YES' }

API レスポンスの形式変換やデータの標準化に使用されます。

19. オブジェクトから特定のプロパティを選択・除外する

javascriptconst userProfile = {
  id: 1,
  username: 'alice123',
  email: 'alice@example.com',
  password: 'hashedPassword',
  firstName: 'Alice',
  lastName: 'Johnson',
  createdAt: '2023-01-01',
  lastLogin: '2023-12-01',
  isAdmin: false,
};

// 特定のプロパティのみを選択
const publicProfile = _.pick(userProfile, [
  'id',
  'username',
  'firstName',
  'lastName',
]);
console.log(publicProfile);
// { id: 1, username: 'alice123', firstName: 'Alice', lastName: 'Johnson' }

// 機密情報を除外
const safeProfile = _.omit(userProfile, [
  'password',
  'lastLogin',
]);
console.log(Object.keys(safeProfile)); // password, lastLoginが含まれない

// 条件に基づいてプロパティを選択
const adminFields = _.pickBy(userProfile, (value, key) => {
  // 管理者のみが見ることができるフィールド
  return (
    key === 'isAdmin' ||
    key.includes('created') ||
    key.includes('Login')
  );
});
console.log(adminFields); // { createdAt: '2023-01-01', lastLogin: '2023-12-01', isAdmin: false }

セキュリティ対策や API レスポンスの調整に重要な機能です。

20. ネストしたオブジェクトの操作

javascriptconst company = {
  name: 'Tech Corp',
  address: {
    street: '123 Tech Street',
    city: 'San Francisco',
    country: 'USA',
    coordinates: {
      lat: 37.7749,
      lng: -122.4194,
    },
  },
  employees: [
    { name: 'Alice', department: 'Engineering' },
    { name: 'Bob', department: 'Marketing' },
  ],
};

// ネストしたプロパティを安全に取得
const city = _.get(company, 'address.city');
const lat = _.get(company, 'address.coordinates.lat');
const invalidPath = _.get(
  company,
  'address.postal.code',
  'N/A'
); // デフォルト値

console.log(city); // 'San Francisco'
console.log(lat); // 37.7749
console.log(invalidPath); // 'N/A'

// ネストしたプロパティを安全に設定
const updatedCompany = _.cloneDeep(company);
_.set(updatedCompany, 'address.coordinates.elevation', 150);
_.set(updatedCompany, 'contact.phone', '+1-555-0123');

console.log(
  _.get(updatedCompany, 'address.coordinates.elevation')
); // 150
console.log(_.get(updatedCompany, 'contact.phone')); // '+1-555-0123'

// ネストしたプロパティが存在するかチェック
const hasCoordinates = _.has(
  company,
  'address.coordinates'
);
const hasPostalCode = _.has(company, 'address.postal.code');
console.log(hasCoordinates); // true
console.log(hasPostalCode); // false

複雑なデータ構造を安全に操作するために欠かせない機能です。

オブジェクトの検索・抽出

オブジェクトから条件に一致するデータを検索したり抽出したりする操作について説明します。

21. オブジェクトの値で検索する

javascriptconst inventory = {
  electronics: {
    laptop: { price: 1200, stock: 5 },
    phone: { price: 800, stock: 10 },
    tablet: { price: 600, stock: 3 },
  },
  books: {
    novel: { price: 15, stock: 20 },
    textbook: { price: 80, stock: 8 },
  },
};

// 条件に一致する値を検索
const expensiveItems = {};
_.forOwn(inventory, (category, categoryName) => {
  const expensive = _.pickBy(
    category,
    (item) => item.price > 100
  );
  if (!_.isEmpty(expensive)) {
    expensiveItems[categoryName] = expensive;
  }
});

console.log(expensiveItems);
// { electronics: { laptop: { price: 1200, stock: 5 }, phone: { price: 800, stock: 10 }, tablet: { price: 600, stock: 3 } } }

// 在庫が少ない商品を検索
const lowStockItems = {};
_.forOwn(inventory, (category, categoryName) => {
  const lowStock = _.pickBy(
    category,
    (item) => item.stock < 5
  );
  if (!_.isEmpty(lowStock)) {
    lowStockItems[categoryName] = lowStock;
  }
});

console.log(lowStockItems);
// { electronics: { tablet: { price: 600, stock: 3 } } }

在庫管理や商品検索システムでよく使用されるパターンです。

22. オブジェクトの配列から条件に一致するオブジェクトを検索

javascriptconst users = {
  user1: {
    name: 'Alice',
    age: 25,
    role: 'admin',
    active: true,
  },
  user2: {
    name: 'Bob',
    age: 30,
    role: 'user',
    active: false,
  },
  user3: {
    name: 'Charlie',
    age: 35,
    role: 'admin',
    active: true,
  },
  user4: {
    name: 'David',
    age: 28,
    role: 'user',
    active: true,
  },
};

// 条件に一致する最初のユーザーを検索
const firstAdmin = _.find(users, { role: 'admin' });
console.log(firstAdmin); // { name: 'Alice', age: 25, role: 'admin', active: true }

// アクティブな管理者を検索
const activeAdmin = _.find(
  users,
  (user) => user.role === 'admin' && user.active
);
console.log(activeAdmin); // { name: 'Alice', age: 25, role: 'admin', active: true }

// 条件に一致するユーザーIDを検索
const adminUserId = _.findKey(users, { role: 'admin' });
console.log(adminUserId); // 'user1'

// 30歳以上のユーザーIDを検索
const matureUserId = _.findKey(
  users,
  (user) => user.age >= 30
);
console.log(matureUserId); // 'user2'

ユーザー管理やセッション管理でよく使用されるパターンです。

オブジェクトのマージ・結合

複数のオブジェクトを結合したり、既存のオブジェクトに新しい情報を追加したりする操作について説明します。

23. オブジェクトの浅いマージ

javascriptconst baseUser = {
  id: 1,
  name: 'Alice',
  email: 'alice@example.com',
};

const userPreferences = {
  theme: 'dark',
  language: 'en',
  notifications: true,
};

const userPermissions = {
  role: 'admin',
  permissions: ['read', 'write'],
};

// 複数のオブジェクトをマージ
const completeUser = _.assign(
  {},
  baseUser,
  userPreferences,
  userPermissions
);
console.log(completeUser);
// {
//   id: 1,
//   name: 'Alice',
//   email: 'alice@example.com',
//   theme: 'dark',
//   language: 'en',
//   notifications: true,
//   role: 'admin',
//   permissions: ['read', 'write']
// }

// プロパティの上書き
const userUpdate = {
  name: 'Alice Johnson',
  theme: 'light',
};

const updatedUser = _.assign({}, completeUser, userUpdate);
console.log(updatedUser.name); // 'Alice Johnson'
console.log(updatedUser.theme); // 'light'

ユーザー情報の更新や設定の結合でよく使用されます。

24. オブジェクトの深いマージ

javascriptconst defaultSettings = {
  ui: {
    theme: 'light',
    sidebar: {
      collapsed: false,
      width: 250,
    },
  },
  api: {
    timeout: 5000,
    retries: 3,
  },
};

const userSettings = {
  ui: {
    theme: 'dark',
    sidebar: {
      collapsed: true,
    },
  },
  api: {
    timeout: 10000,
  },
};

// 深いマージ
const finalSettings = _.merge(
  {},
  defaultSettings,
  userSettings
);
console.log(finalSettings);
// {
//   ui: {
//     theme: 'dark',           // userSettingsで上書き
//     sidebar: {
//       collapsed: true,       // userSettingsで上書き
//       width: 250            // defaultSettingsから継承
//     }
//   },
//   api: {
//     timeout: 10000,         // userSettingsで上書き
//     retries: 3              // defaultSettingsから継承
//   }
// }

// カスタムマージ関数を使用
const customMerge = _.mergeWith(
  {},
  defaultSettings,
  userSettings,
  (objValue, srcValue) => {
    // 配列の場合は結合する
    if (_.isArray(objValue)) {
      return objValue.concat(srcValue);
    }
  }
);

設定管理や複雑なオブジェクト構造の結合に使用されます。

25. 条件付きマージ

javascriptconst baseProduct = {
  id: 1,
  name: 'Smartphone',
  price: 500,
  inStock: true,
};

const priceUpdate = { price: 450 };
const stockUpdate = {
  inStock: false,
  lastSold: '2023-12-01',
};
const reviewUpdate = { rating: 4.5, reviewCount: 128 };

// 条件に基づいてマージ
const updateProduct = (product, updates, condition) => {
  return condition
    ? _.assign({}, product, updates)
    : product;
};

const isOnSale = true;
const hasNewReviews = true;
const isOutOfStock = false;

let updatedProduct = updateProduct(
  baseProduct,
  priceUpdate,
  isOnSale
);
updatedProduct = updateProduct(
  updatedProduct,
  reviewUpdate,
  hasNewReviews
);
updatedProduct = updateProduct(
  updatedProduct,
  stockUpdate,
  isOutOfStock
);

console.log(updatedProduct);
// { id: 1, name: 'Smartphone', price: 450, inStock: true, rating: 4.5, reviewCount: 128 }

// より複雑な条件付きマージ
const conditionalUpdates = [
  { data: priceUpdate, condition: () => isOnSale },
  { data: stockUpdate, condition: () => isOutOfStock },
  { data: reviewUpdate, condition: () => hasNewReviews },
];

const finalProduct = conditionalUpdates.reduce(
  (product, update) => {
    return update.condition()
      ? _.assign({}, product, update.data)
      : product;
  },
  baseProduct
);

console.log(finalProduct.price); // 450 (セール中なので更新)
console.log(finalProduct.inStock); // true (在庫切れでないので更新されない)

状態管理やビジネスロジックの実装でよく使用されるパターンです。

配列とオブジェクトの相互変換レシピ

配列とオブジェクトの相互変換は、データ形式の変更や API レスポンスの加工でよく必要になる操作です。効率的な変換方法を学ぶことで、柔軟なデータ処理が可能になります。

配列からオブジェクトへの変換

配列のデータをオブジェクト形式に変換することで、キーによる高速なデータアクセスが可能になります。

26. 配列をキー・値のペアのオブジェクトに変換

javascript// ユーザー配列をIDをキーとするオブジェクトに変換
const users = [
  { id: 1, name: 'Alice', email: 'alice@example.com' },
  { id: 2, name: 'Bob', email: 'bob@example.com' },
  { id: 3, name: 'Charlie', email: 'charlie@example.com' },
];

// keyByを使用してIDをキーとするオブジェクトを作成
const usersById = _.keyBy(users, 'id');
console.log(usersById);
// {
//   '1': { id: 1, name: 'Alice', email: 'alice@example.com' },
//   '2': { id: 2, name: 'Bob', email: 'bob@example.com' },
//   '3': { id: 3, name: 'Charlie', email: 'charlie@example.com' }
// }

// カスタム関数でキーを生成
const usersByEmail = _.keyBy(
  users,
  (user) => user.email.split('@')[0]
);
console.log(usersByEmail);
// {
//   'alice': { id: 1, name: 'Alice', email: 'alice@example.com' },
//   'bob': { id: 2, name: 'Bob', email: 'bob@example.com' },
//   'charlie': { id: 3, name: 'Charlie', email: 'charlie@example.com' }
// }

// 名前の長さでキーを作成
const usersByNameLength = _.keyBy(
  users,
  (user) => user.name.length
);
console.log(Object.keys(usersByNameLength)); // ['5', '3', '7']

データベースのレコードをメモリ上で高速検索するためのインデックス作成に使用されます。

27. 配列の要素を集約してオブジェクトを作成

javascriptconst orders = [
  { customerId: 1, product: 'Laptop', amount: 1200 },
  { customerId: 2, product: 'Phone', amount: 800 },
  { customerId: 1, product: 'Mouse', amount: 25 },
  { customerId: 3, product: 'Tablet', amount: 600 },
  { customerId: 2, product: 'Keyboard', amount: 100 },
];

// 顧客ごとの合計金額を計算
const customerTotals = orders.reduce((acc, order) => {
  const customerId = order.customerId;
  acc[customerId] = (acc[customerId] || 0) + order.amount;
  return acc;
}, {});
console.log(customerTotals); // { '1': 1225, '2': 900, '3': 600 }

// Lodashを使用した同様の処理
const customerTotalsLodash = _(orders)
  .groupBy('customerId')
  .mapValues((customerOrders) =>
    _.sumBy(customerOrders, 'amount')
  )
  .value();
console.log(customerTotalsLodash); // { '1': 1225, '2': 900, '3': 600 }

// 商品カテゴリ別の統計
const products = [
  {
    name: 'Laptop',
    category: 'Electronics',
    price: 1200,
    sales: 50,
  },
  {
    name: 'Book',
    category: 'Education',
    price: 20,
    sales: 200,
  },
  {
    name: 'Phone',
    category: 'Electronics',
    price: 800,
    sales: 120,
  },
  {
    name: 'Pen',
    category: 'Office',
    price: 5,
    sales: 1000,
  },
];

const categoryStats = _(products)
  .groupBy('category')
  .mapValues((categoryProducts) => ({
    totalRevenue: _.sumBy(
      categoryProducts,
      (product) => product.price * product.sales
    ),
    averagePrice: _.meanBy(categoryProducts, 'price'),
    productCount: categoryProducts.length,
  }))
  .value();

console.log(categoryStats);
// {
//   Electronics: { totalRevenue: 156000, averagePrice: 1000, productCount: 2 },
//   Education: { totalRevenue: 4000, averagePrice: 20, productCount: 1 },
//   Office: { totalRevenue: 5000, averagePrice: 5, productCount: 1 }
// }

売上分析やデータ集計処理でよく使用されるパターンです。

28. 多次元配列をネストしたオブジェクトに変換

javascript// パスとその値のペアの配列
const pathValuePairs = [
  ['user.profile.name', 'Alice'],
  ['user.profile.age', 25],
  ['user.settings.theme', 'dark'],
  ['user.settings.notifications.email', true],
  ['user.settings.notifications.push', false],
  ['app.version', '1.2.3'],
  ['app.features.chat', true],
];

// ネストしたオブジェクトを構築
const nestedConfig = {};
pathValuePairs.forEach(([path, value]) => {
  _.set(nestedConfig, path, value);
});

console.log(nestedConfig);
// {
//   user: {
//     profile: { name: 'Alice', age: 25 },
//     settings: {
//       theme: 'dark',
//       notifications: { email: true, push: false }
//     }
//   },
//   app: {
//     version: '1.2.3',
//     features: { chat: true }
//   }
// }

// より実用的な例:フォームデータの変換
const formData = [
  { name: 'user[name]', value: 'Bob' },
  { name: 'user[email]', value: 'bob@example.com' },
  { name: 'preferences[theme]', value: 'light' },
  { name: 'preferences[language]', value: 'en' },
];

const formObject = {};
formData.forEach((field) => {
  // 配列記法をドット記法に変換
  const path = field.name.replace(/\[(\w+)\]/g, '.$1');
  _.set(formObject, path, field.value);
});

console.log(formObject);
// {
//   user: { name: 'Bob', email: 'bob@example.com' },
//   preferences: { theme: 'light', language: 'en' }
// }

フォーム処理や設定データの変換でよく使用されます。

オブジェクトから配列への変換

オブジェクトのデータを配列に変換することで、ループ処理や配列メソッドの利用が可能になります。

29. オブジェクトのプロパティを配列に変換

javascriptconst userStats = {
  alice: { posts: 45, followers: 120, following: 80 },
  bob: { posts: 23, followers: 95, following: 110 },
  charlie: { posts: 67, followers: 200, following: 150 },
};

// キーの配列を取得
const usernames = _.keys(userStats);
console.log(usernames); // ['alice', 'bob', 'charlie']

// 値の配列を取得
const statsValues = _.values(userStats);
console.log(statsValues);
// [
//   { posts: 45, followers: 120, following: 80 },
//   { posts: 23, followers: 95, following: 110 },
//   { posts: 67, followers: 200, following: 150 }
// ]

// キーと値のペアの配列を取得
const userEntries = _.toPairs(userStats);
console.log(userEntries);
// [
//   ['alice', { posts: 45, followers: 120, following: 80 }],
//   ['bob', { posts: 23, followers: 95, following: 110 }],
//   ['charlie', { posts: 67, followers: 200, following: 150 }]
// ]

// ユーザー名を含むオブジェクトの配列を作成
const usersWithNames = _.map(
  userStats,
  (stats, username) => ({
    username,
    ...stats,
  })
);
console.log(usersWithNames);
// [
//   { username: 'alice', posts: 45, followers: 120, following: 80 },
//   { username: 'bob', posts: 23, followers: 95, following: 110 },
//   { username: 'charlie', posts: 67, followers: 200, following: 150 }
// ]

データの表示やソート処理でよく使用されるパターンです。

30. ネストしたオブジェクトを平坦な配列に変換

javascriptconst organizationStructure = {
  engineering: {
    frontend: ['Alice', 'Bob'],
    backend: ['Charlie', 'David'],
    devops: ['Eve'],
  },
  marketing: {
    digital: ['Frank', 'Grace'],
    content: ['Henry'],
  },
  sales: {
    enterprise: ['Ivy', 'Jack'],
    retail: ['Kate'],
  },
};

// 全従業員の配列を作成
const allEmployees = _(organizationStructure)
  .values()
  .flatMap(_.values)
  .flatten()
  .value();
console.log(allEmployees);
// ['Alice', 'Bob', 'Charlie', 'David', 'Eve', 'Frank', 'Grace', 'Henry', 'Ivy', 'Jack', 'Kate']

// 部署と従業員の関係を含む配列を作成
const employeeDetails = [];
_.forOwn(
  organizationStructure,
  (departments, divisionName) => {
    _.forOwn(departments, (employees, departmentName) => {
      employees.forEach((employee) => {
        employeeDetails.push({
          name: employee,
          division: divisionName,
          department: departmentName,
          fullPath: `${divisionName}.${departmentName}`,
        });
      });
    });
  }
);

console.log(employeeDetails.slice(0, 3));
// [
//   { name: 'Alice', division: 'engineering', department: 'frontend', fullPath: 'engineering.frontend' },
//   { name: 'Bob', division: 'engineering', department: 'frontend', fullPath: 'engineering.frontend' },
//   { name: 'Charlie', division: 'engineering', department: 'backend', fullPath: 'engineering.backend' }
// ]

// 部署ごとの従業員数を配列で取得
const departmentSizes = _(organizationStructure)
  .mapValues((departments) =>
    _.mapValues(
      departments,
      (employees) => employees.length
    )
  )
  .toPairs()
  .flatMap(([division, departments]) =>
    _.map(departments, (size, department) => ({
      division,
      department,
      size,
      fullName: `${division} ${department}`,
    }))
  )
  .value();

console.log(departmentSizes);
// [
//   { division: 'engineering', department: 'frontend', size: 2, fullName: 'engineering frontend' },
//   { division: 'engineering', department: 'backend', size: 2, fullName: 'engineering backend' },
//   ...
// ]

組織図の処理やレポート生成でよく使用されるパターンです。

ネストしたデータの変換

複雑なネスト構造を持つデータの変換は、実際の開発でよく遭遇する課題です。

31. 階層構造のデータを平坦化

javascriptconst menuStructure = {
  id: 1,
  name: 'Dashboard',
  children: [
    {
      id: 2,
      name: 'Analytics',
      children: [
        { id: 3, name: 'Reports', children: [] },
        { id: 4, name: 'Charts', children: [] },
      ],
    },
    {
      id: 5,
      name: 'Users',
      children: [
        { id: 6, name: 'List', children: [] },
        { id: 7, name: 'Roles', children: [] },
      ],
    },
  ],
};

// 階層構造を平坦な配列に変換
const flattenMenu = (menu, level = 0, parent = null) => {
  const result = [
    {
      id: menu.id,
      name: menu.name,
      level,
      parent,
      path: parent
        ? `${parent.path}/${menu.name}`
        : menu.name,
    },
  ];

  if (menu.children && menu.children.length > 0) {
    menu.children.forEach((child) => {
      result.push(
        ...flattenMenu(child, level + 1, {
          id: menu.id,
          name: menu.name,
          path: parent
            ? `${parent.path}/${menu.name}`
            : menu.name,
        })
      );
    });
  }

  return result;
};

const flatMenu = flattenMenu(menuStructure);
console.log(flatMenu);
// [
//   { id: 1, name: 'Dashboard', level: 0, parent: null, path: 'Dashboard' },
//   { id: 2, name: 'Analytics', level: 1, parent: { id: 1, name: 'Dashboard', path: 'Dashboard' }, path: 'Dashboard/Analytics' },
//   { id: 3, name: 'Reports', level: 2, parent: { id: 2, name: 'Analytics', path: 'Dashboard/Analytics' }, path: 'Dashboard/Analytics/Reports' },
//   ...
// ]

// Lodashのtransformを使用した実装
const flattenMenuLodash = (
  menu,
  level = 0,
  parentPath = ''
) => {
  return _.transform(
    menu,
    (result, value, key) => {
      if (key === 'children' && _.isArray(value)) {
        value.forEach((child) => {
          result.push(
            ...flattenMenuLodash(
              child,
              level + 1,
              parentPath
                ? `${parentPath}/${menu.name}`
                : menu.name
            )
          );
        });
      } else if (key !== 'children') {
        if (!result.current) {
          result.current = { level, parentPath };
        }
        result.current[key] = value;
      }
    },
    []
  );
};

ナビゲーションメニューやツリー構造の処理でよく使用されます。

32. 平坦なデータを階層構造に変換

javascriptconst flatEmployees = [
  { id: 1, name: 'CEO', parentId: null },
  { id: 2, name: 'CTO', parentId: 1 },
  { id: 3, name: 'CFO', parentId: 1 },
  { id: 4, name: 'Frontend Lead', parentId: 2 },
  { id: 5, name: 'Backend Lead', parentId: 2 },
  { id: 6, name: 'Frontend Dev 1', parentId: 4 },
  { id: 7, name: 'Frontend Dev 2', parentId: 4 },
  { id: 8, name: 'Backend Dev 1', parentId: 5 },
];

// 平坦なデータを階層構造に変換
const buildHierarchy = (employees) => {
  // IDをキーとするマップを作成
  const employeeMap = _.keyBy(employees, 'id');

  // 各従業員にchildrenプロパティを追加
  const employeesWithChildren = _.mapValues(
    employeeMap,
    (employee) => ({
      ...employee,
      children: [],
    })
  );

  let root = null;

  // 親子関係を構築
  _.forEach(employeesWithChildren, (employee) => {
    if (employee.parentId === null) {
      root = employee;
    } else {
      const parent =
        employeesWithChildren[employee.parentId];
      if (parent) {
        parent.children.push(employee);
      }
    }
  });

  return root;
};

const organizationChart = buildHierarchy(flatEmployees);
console.log(JSON.stringify(organizationChart, null, 2));

// 特定の階層レベルの従業員を取得
const getEmployeesByLevel = (
  orgChart,
  targetLevel,
  currentLevel = 0
) => {
  if (currentLevel === targetLevel) {
    return [orgChart];
  }

  if (
    !orgChart.children ||
    orgChart.children.length === 0
  ) {
    return [];
  }

  return _.flatMap(orgChart.children, (child) =>
    getEmployeesByLevel(
      child,
      targetLevel,
      currentLevel + 1
    )
  );
};

const level2Employees = getEmployeesByLevel(
  organizationChart,
  2
);
console.log(_.map(level2Employees, 'name')); // ['Frontend Lead', 'Backend Lead']

// 組織図の深さを計算
const calculateDepth = (node) => {
  if (!node.children || node.children.length === 0) {
    return 1;
  }
  return 1 + _.max(_.map(node.children, calculateDepth));
};

const orgDepth = calculateDepth(organizationChart);
console.log(`組織の階層の深さ: ${orgDepth}`); // 組織の階層の深さ: 4

組織図やカテゴリ管理システムでよく使用されるパターンです。

実践的な応用レシピ

実際の開発現場でよく遭遇するシナリオに基づいた、より実践的な Lodash の活用方法をご紹介します。これらのレシピは、日常的な開発作業で直面する具体的な課題を解決するためのものです。

API レスポンスの加工

API から取得したデータを、フロントエンドで使いやすい形式に変換する処理は開発において頻繁に行われます。

33. REST API レスポンスの正規化

javascript// API レスポンスの例
const apiResponse = {
  data: [
    {
      user_id: 1,
      user_name: 'alice_johnson',
      user_email: 'alice@example.com',
      profile_image_url: 'https://example.com/avatar1.jpg',
      created_at: '2023-01-15T10:30:00Z',
      last_login_at: '2023-12-01T14:20:00Z',
      is_active: true,
      role: 'admin',
    },
    {
      user_id: 2,
      user_name: 'bob_smith',
      user_email: 'bob@example.com',
      profile_image_url: null,
      created_at: '2023-02-20T09:15:00Z',
      last_login_at: '2023-11-28T16:45:00Z',
      is_active: false,
      role: 'user',
    },
  ],
  meta: {
    total_count: 150,
    current_page: 1,
    per_page: 20,
  },
};

// フロントエンド用に正規化
const normalizeUserResponse = (response) => {
  const users = _.map(response.data, (user) => ({
    // キー名をキャメルケースに変換
    id: user.user_id,
    username: user.user_name,
    email: user.user_email,
    avatar: user.profile_image_url || '/default-avatar.png', // デフォルト値を設定
    createdAt: new Date(user.created_at), // 日付オブジェクトに変換
    lastLoginAt: user.last_login_at
      ? new Date(user.last_login_at)
      : null,
    isActive: user.is_active,
    role: user.role,
    // 計算プロパティを追加
    displayName: _.startCase(
      user.user_name.replace('_', ' ')
    ),
    isOnline:
      user.last_login_at &&
      new Date() - new Date(user.last_login_at) <
        30 * 60 * 1000, // 30分以内
  }));

  // ページネーション情報も正規化
  const pagination = {
    total: response.meta.total_count,
    currentPage: response.meta.current_page,
    perPage: response.meta.per_page,
    totalPages: Math.ceil(
      response.meta.total_count / response.meta.per_page
    ),
  };

  return { users, pagination };
};

const normalizedData = normalizeUserResponse(apiResponse);
console.log(normalizedData.users[0]);
// {
//   id: 1,
//   username: 'alice_johnson',
//   email: 'alice@example.com',
//   avatar: 'https://example.com/avatar1.jpg',
//   createdAt: Date object,
//   lastLoginAt: Date object,
//   isActive: true,
//   role: 'admin',
//   displayName: 'Alice Johnson',
//   isOnline: false
// }

API レスポンスの標準化により、フロントエンドでの一貫したデータ処理が可能になります。

34. GraphQL レスポンスの変換

javascript// GraphQL API レスポンスの例
const graphqlResponse = {
  data: {
    posts: {
      edges: [
        {
          node: {
            id: '1',
            title: 'Introduction to React',
            content: 'React is a JavaScript library...',
            publishedAt: '2023-11-01T00:00:00Z',
            author: {
              id: '10',
              name: 'Alice Johnson',
              avatar: {
                url: 'https://example.com/alice.jpg',
              },
            },
            tags: {
              edges: [
                { node: { id: 't1', name: 'React' } },
                { node: { id: 't2', name: 'JavaScript' } },
              ],
            },
            comments: {
              totalCount: 15,
            },
          },
        },
        {
          node: {
            id: '2',
            title: 'Advanced TypeScript',
            content: 'TypeScript provides static typing...',
            publishedAt: '2023-11-15T00:00:00Z',
            author: {
              id: '11',
              name: 'Bob Smith',
              avatar: {
                url: 'https://example.com/bob.jpg',
              },
            },
            tags: {
              edges: [
                { node: { id: 't3', name: 'TypeScript' } },
                { node: { id: 't2', name: 'JavaScript' } },
              ],
            },
            comments: {
              totalCount: 8,
            },
          },
        },
      ],
      pageInfo: {
        hasNextPage: true,
        endCursor: 'cursor123',
      },
    },
  },
};

// GraphQL の Relay 形式から通常の配列形式に変換
const transformGraphQLResponse = (response) => {
  const posts = _.map(
    response.data.posts.edges,
    ({ node }) => ({
      id: node.id,
      title: node.title,
      content: node.content,
      publishedAt: new Date(node.publishedAt),
      author: {
        id: node.author.id,
        name: node.author.name,
        avatar: node.author.avatar.url,
      },
      tags: _.map(node.tags.edges, ({ node: tag }) => ({
        id: tag.id,
        name: tag.name,
      })),
      commentCount: node.comments.totalCount,
      // 計算プロパティ
      excerpt: _.truncate(node.content, { length: 100 }),
      readingTime: Math.ceil(
        node.content.split(' ').length / 200
      ), //
      slug: _.kebabCase(node.title),
    })
  );

  const pagination = {
    hasNextPage: response.data.posts.pageInfo.hasNextPage,
    endCursor: response.data.posts.pageInfo.endCursor,
  };

  // タグの出現頻度を計算
  const tagFrequency = _(posts)
    .flatMap('tags')
    .countBy('name')
    .toPairs()
    .orderBy(1, 'desc')
    .take(5)
    .fromPairs()
    .value();

  return { posts, pagination, tagFrequency };
};

const transformedData =
  transformGraphQLResponse(graphqlResponse);
console.log(transformedData.posts[0].excerpt);
console.log(transformedData.tagFrequency); // { JavaScript: 2, React: 1, TypeScript: 1 }

GraphQL の複雑なネスト構造を扱いやすい形式に変換できます。

35. エラーレスポンスの統一処理

javascript// 様々な形式のエラーレスポンス
const errorResponses = [
  // RESTful API エラー
  {
    status: 400,
    error: {
      message: 'Validation failed',
      details: [
        { field: 'email', message: 'Invalid email format' },
        {
          field: 'password',
          message: 'Password too short',
        },
      ],
    },
  },

  // GraphQL エラー
  {
    errors: [
      {
        message: 'User not found',
        path: ['user'],
        extensions: { code: 'USER_NOT_FOUND' },
      },
    ],
  },

  // カスタム API エラー
  {
    success: false,
    errorCode: 'INVALID_TOKEN',
    errorMessage: 'Authentication token is invalid',
  },
];

// エラーレスポンスを統一形式に変換
const normalizeError = (errorResponse) => {
  // RESTful API エラーの場合
  if (errorResponse.error && errorResponse.status) {
    return {
      type: 'validation',
      message: errorResponse.error.message,
      code: errorResponse.status,
      details: _.map(
        errorResponse.error.details || [],
        (detail) => ({
          field: detail.field,
          message: detail.message,
        })
      ),
    };
  }

  // GraphQL エラーの場合
  if (
    errorResponse.errors &&
    _.isArray(errorResponse.errors)
  ) {
    const primaryError = errorResponse.errors[0];
    return {
      type: 'graphql',
      message: primaryError.message,
      code: _.get(
        primaryError,
        'extensions.code',
        'UNKNOWN'
      ),
      path: primaryError.path,
      details: _.map(errorResponse.errors, (error) => ({
        message: error.message,
        path: error.path,
      })),
    };
  }

  // カスタム API エラーの場合
  if (errorResponse.success === false) {
    return {
      type: 'custom',
      message: errorResponse.errorMessage,
      code: errorResponse.errorCode,
      details: [],
    };
  }

  // 不明な形式の場合
  return {
    type: 'unknown',
    message: 'An unknown error occurred',
    code: 'UNKNOWN_ERROR',
    details: [],
    original: errorResponse,
  };
};

// エラーメッセージの表示用テキストを生成
const generateErrorMessage = (normalizedError) => {
  let message = normalizedError.message;

  if (normalizedError.details.length > 0) {
    const detailMessages = _.map(
      normalizedError.details,
      (detail) => {
        return detail.field
          ? `${detail.field}: ${detail.message}`
          : detail.message;
      }
    );
    message += '\n詳細:\n' + detailMessages.join('\n');
  }

  return message;
};

// 実際の使用例
const processErrors = (responses) => {
  return _.map(responses, (response) => {
    const normalized = normalizeError(response);
    return {
      ...normalized,
      displayMessage: generateErrorMessage(normalized),
      isRetryable: [
        'NETWORK_ERROR',
        'TIMEOUT',
        'SERVER_ERROR',
      ].includes(normalized.code),
    };
  });
};

const processedErrors = processErrors(errorResponses);
console.log(processedErrors[0].displayMessage);
// Validation failed
// 詳細:
// email: Invalid email format
// password: Password too short

異なる API からのエラーレスポンスを統一的に処理できるようになります。

フォームデータの処理

ユーザーから入力されたフォームデータの検証、変換、送信処理は Web アプリケーション開発の基本的な作業です。

36. 複雑なフォームデータの検証と変換

javascript// フォームから送信されたデータ
const formData = {
  // 基本情報
  firstName: '  Alice  ',
  lastName: '  Johnson  ',
  email: 'ALICE@EXAMPLE.COM',
  phone: '090-1234-5678',
  birthDate: '1990-05-15',

  // 住所情報
  'address.street': '123 Main St',
  'address.city': 'Tokyo',
  'address.zipCode': '100-0001',

  // 設定
  'preferences.newsletter': 'true',
  'preferences.notifications.email': 'false',
  'preferences.notifications.sms': 'true',
  'preferences.theme': 'dark',

  // 趣味(複数選択)
  hobbies: ['reading', 'swimming', 'cooking'],

  // その他
  bio: 'I am a software developer...',
  agreeToTerms: 'on',
};

// フォームデータの検証と変換
const validateAndTransformForm = (data) => {
  // 基本的なデータクリーニング
  const cleaned = _.mapValues(data, (value) => {
    if (_.isString(value)) {
      return value.trim();
    }
    return value;
  });

  // ネストしたオブジェクト構造を構築
  const nested = {};
  _.forEach(cleaned, (value, key) => {
    if (key.includes('.')) {
      _.set(nested, key, value);
    } else {
      nested[key] = value;
    }
  });

  // データ変換とバリデーション
  const transformed = {
    // 名前の正規化
    name: {
      first: _.startCase(nested.firstName.toLowerCase()),
      last: _.startCase(nested.lastName.toLowerCase()),
      full: `${_.startCase(
        nested.firstName.toLowerCase()
      )} ${_.startCase(nested.lastName.toLowerCase())}`,
    },

    // メールアドレスの正規化
    email: nested.email.toLowerCase(),

    // 電話番号の正規化
    phone: nested.phone.replace(/-/g, ''),

    // 生年月日の変換
    birthDate: new Date(nested.birthDate),

    // 住所情報
    address: {
      street: nested.address.street,
      city: nested.address.city,
      zipCode: nested.address.zipCode,
      formatted: `${nested.address.street}, ${nested.address.city} ${nested.address.zipCode}`,
    },

    // 設定情報(文字列のbooleanを変換)
    preferences: {
      newsletter: nested.preferences.newsletter === 'true',
      notifications: {
        email:
          nested.preferences.notifications.email === 'true',
        sms:
          nested.preferences.notifications.sms === 'true',
      },
      theme: nested.preferences.theme,
    },

    // 趣味リストの正規化
    hobbies: _.map(nested.hobbies, (hobby) =>
      _.startCase(hobby)
    ),

    // バイオグラフィの処理
    bio: {
      text: nested.bio,
      wordCount: nested.bio.split(' ').length,
      charCount: nested.bio.length,
    },

    // チェックボックスの処理
    agreeToTerms: nested.agreeToTerms === 'on',

    // メタデータ
    submittedAt: new Date(),
    age:
      new Date().getFullYear() -
      new Date(nested.birthDate).getFullYear(),
  };

  // バリデーションエラーのチェック
  const errors = [];

  if (!transformed.email.includes('@')) {
    errors.push({
      field: 'email',
      message: 'Invalid email format',
    });
  }

  if (transformed.phone.length !== 11) {
    errors.push({
      field: 'phone',
      message: 'Phone number must be 11 digits',
    });
  }

  if (!transformed.agreeToTerms) {
    errors.push({
      field: 'agreeToTerms',
      message: 'Must agree to terms',
    });
  }

  if (transformed.age < 18) {
    errors.push({
      field: 'birthDate',
      message: 'Must be 18 or older',
    });
  }

  return {
    data: transformed,
    errors,
    isValid: errors.length === 0,
  };
};

const result = validateAndTransformForm(formData);
console.log(result.data.name.full); // "Alice Johnson"
console.log(result.data.preferences.newsletter); // true
console.log(result.errors); // バリデーションエラーの配列

フォームデータの一括処理により、データの品質と一貫性を保つことができます。

37. 動的フォームフィールドの処理

javascript// 動的に追加されるフォームフィールド
const dynamicFormData = {
  // 基本情報
  companyName: 'Tech Corp',

  // 動的に追加された従業員情報
  'employees[0].name': 'Alice',
  'employees[0].email': 'alice@techcorp.com',
  'employees[0].role': 'developer',
  'employees[0].skills[0]': 'JavaScript',
  'employees[0].skills[1]': 'React',

  'employees[1].name': 'Bob',
  'employees[1].email': 'bob@techcorp.com',
  'employees[1].role': 'designer',
  'employees[1].skills[0]': 'Photoshop',
  'employees[1].skills[1]': 'Figma',
  'employees[1].skills[2]': 'CSS',

  // 動的に追加されたプロジェクト情報
  'projects[0].name': 'Website Redesign',
  'projects[0].budget': '50000',
  'projects[0].duration': '6',
  'projects[0].teamMembers[0]': '0', // employee index
  'projects[0].teamMembers[1]': '1',

  'projects[1].name': 'Mobile App',
  'projects[1].budget': '100000',
  'projects[1].duration': '12',
  'projects[1].teamMembers[0]': '0',
};

// 動的フォームデータの変換
const transformDynamicForm = (formData) => {
  // まず、すべてのデータをネストしたオブジェクトに変換
  const nested = {};
  _.forEach(formData, (value, key) => {
    // 配列記法 [0], [1] をドット記法に変換
    const normalizedKey = key.replace(/\[(\d+)\]/g, '.$1');
    _.set(nested, normalizedKey, value);
  });

  // 従業員データの処理
  const employees = _.map(
    nested.employees || {},
    (employee, index) => ({
      id: parseInt(index),
      name: employee.name,
      email: employee.email,
      role: employee.role,
      skills: _.compact(_.values(employee.skills || {})), // 空の値を除去
    })
  );

  // プロジェクトデータの処理
  const projects = _.map(
    nested.projects || {},
    (project, index) => {
      const teamMemberIndices = _.compact(
        _.values(project.teamMembers || {})
      );
      const teamMembers = _.map(
        teamMemberIndices,
        (memberIndex) => {
          return employees[parseInt(memberIndex)];
        }
      ).filter(Boolean); // 存在しないメンバーを除去

      return {
        id: parseInt(index),
        name: project.name,
        budget: parseInt(project.budget),
        duration: parseInt(project.duration),
        teamMembers,
        teamSize: teamMembers.length,
      };
    }
  );

  // 統計情報の計算
  const statistics = {
    totalEmployees: employees.length,
    totalProjects: projects.length,
    totalBudget: _.sumBy(projects, 'budget'),
    averageProjectDuration: _.meanBy(projects, 'duration'),
    skillsDistribution: _(employees)
      .flatMap('skills')
      .countBy()
      .toPairs()
      .orderBy(1, 'desc')
      .fromPairs()
      .value(),
    roleDistribution: _.countBy(employees, 'role'),
  };

  return {
    companyName: nested.companyName,
    employees,
    projects,
    statistics,
    submittedAt: new Date(),
  };
};

const transformedData =
  transformDynamicForm(dynamicFormData);
console.log(transformedData.employees[0]);
// { id: 0, name: 'Alice', email: 'alice@techcorp.com', role: 'developer', skills: ['JavaScript', 'React'] }

console.log(transformedData.statistics.skillsDistribution);
// { JavaScript: 1, React: 1, Photoshop: 1, Figma: 1, CSS: 1 }

console.log(
  transformedData.projects[0].teamMembers.map(
    (member) => member.name
  )
);
// ['Alice', 'Bob']

動的フォームの複雑なデータ構造を効率的に処理できます。

データ分析・集計処理

ビジネスインテリジェンスやレポート機能では、大量のデータから意味のある情報を抽出する処理が必要になります。

38. 売上データの多次元分析

javascript// 売上データの例
const salesData = [
  {
    date: '2023-01-15',
    product: 'Laptop',
    category: 'Electronics',
    amount: 1200,
    quantity: 1,
    region: 'North',
    salesperson: 'Alice',
  },
  {
    date: '2023-01-16',
    product: 'Phone',
    category: 'Electronics',
    amount: 800,
    quantity: 1,
    region: 'South',
    salesperson: 'Bob',
  },
  {
    date: '2023-01-17',
    product: 'Book',
    category: 'Education',
    amount: 25,
    quantity: 2,
    region: 'North',
    salesperson: 'Alice',
  },
  {
    date: '2023-01-18',
    product: 'Laptop',
    category: 'Electronics',
    amount: 1200,
    quantity: 1,
    region: 'East',
    salesperson: 'Charlie',
  },
  {
    date: '2023-01-19',
    product: 'Pen',
    category: 'Office',
    amount: 5,
    quantity: 10,
    region: 'South',
    salesperson: 'Bob',
  },
  {
    date: '2023-01-20',
    product: 'Phone',
    category: 'Electronics',
    amount: 800,
    quantity: 1,
    region: 'West',
    salesperson: 'David',
  },
  {
    date: '2023-02-01',
    product: 'Tablet',
    category: 'Electronics',
    amount: 600,
    quantity: 1,
    region: 'North',
    salesperson: 'Alice',
  },
  {
    date: '2023-02-02',
    product: 'Book',
    category: 'Education',
    amount: 30,
    quantity: 1,
    region: 'East',
    salesperson: 'Charlie',
  },
  {
    date: '2023-02-03',
    product: 'Laptop',
    category: 'Electronics',
    amount: 1200,
    quantity: 2,
    region: 'South',
    salesperson: 'Bob',
  },
];

// 多次元での売上分析
const analyzeSalesData = (data) => {
  // データの前処理
  const processedData = _.map(data, (sale) => ({
    ...sale,
    date: new Date(sale.date),
    month: new Date(sale.date).getMonth() + 1,
    year: new Date(sale.date).getFullYear(),
    revenue: sale.amount * sale.quantity,
  }));

  // 1. カテゴリ別売上分析
  const categoryAnalysis = _(processedData)
    .groupBy('category')
    .mapValues((sales) => ({
      totalRevenue: _.sumBy(sales, 'revenue'),
      totalQuantity: _.sumBy(sales, 'quantity'),
      averageOrderValue: _.meanBy(sales, 'amount'),
      salesCount: sales.length,
      topProduct: _(sales)
        .groupBy('product')
        .mapValues((productSales) =>
          _.sumBy(productSales, 'revenue')
        )
        .toPairs()
        .maxBy(1)[0],
    }))
    .value();

  // 2. 地域別売上分析
  const regionAnalysis = _(processedData)
    .groupBy('region')
    .mapValues((sales) => ({
      totalRevenue: _.sumBy(sales, 'revenue'),
      salesCount: sales.length,
      topSalesperson: _(sales)
        .groupBy('salesperson')
        .mapValues((personSales) =>
          _.sumBy(personSales, 'revenue')
        )
        .toPairs()
        .maxBy(1)[0],
      monthlyTrend: _(sales)
        .groupBy('month')
        .mapValues((monthlySales) =>
          _.sumBy(monthlySales, 'revenue')
        )
        .value(),
    }))
    .value();

  // 3. 営業担当者別売上分析
  const salespersonAnalysis = _(processedData)
    .groupBy('salesperson')
    .mapValues((sales) => ({
      totalRevenue: _.sumBy(sales, 'revenue'),
      salesCount: sales.length,
      averageSaleAmount: _.meanBy(sales, 'revenue'),
      bestMonth: _(sales)
        .groupBy('month')
        .mapValues((monthlySales) =>
          _.sumBy(monthlySales, 'revenue')
        )
        .toPairs()
        .maxBy(1),
      strongestCategory: _(sales)
        .groupBy('category')
        .mapValues((categorySales) =>
          _.sumBy(categorySales, 'revenue')
        )
        .toPairs()
        .maxBy(1)[0],
    }))
    .value();

  // 4. 時系列分析
  const timeSeriesAnalysis = _(processedData)
    .groupBy(
      (sale) =>
        `${sale.year}-${sale.month
          .toString()
          .padStart(2, '0')}`
    )
    .mapValues((sales) => ({
      totalRevenue: _.sumBy(sales, 'revenue'),
      salesCount: sales.length,
      averageOrderValue: _.meanBy(sales, 'revenue'),
      uniqueCustomers: _.uniqBy(sales, 'salesperson')
        .length,
    }))
    .toPairs()
    .sortBy(0)
    .value();

  // 5. 商品別パフォーマンス
  const productAnalysis = _(processedData)
    .groupBy('product')
    .mapValues((sales) => {
      const totalRevenue = _.sumBy(sales, 'revenue');
      const totalQuantity = _.sumBy(sales, 'quantity');

      return {
        totalRevenue,
        totalQuantity,
        averagePrice: totalRevenue / totalQuantity,
        salesVelocity:
          totalQuantity / _.uniqBy(sales, 'date').length, // 日割り販売数
        regionSpread: _.uniq(_.map(sales, 'region')).length,
        lastSaleDate: _.maxBy(sales, 'date').date,
      };
    })
    .toPairs()
    .orderBy(([, data]) => data.totalRevenue, 'desc')
    .value();

  // 6. 総合サマリー
  const overallSummary = {
    totalRevenue: _.sumBy(processedData, 'revenue'),
    totalSales: processedData.length,
    averageOrderValue: _.meanBy(processedData, 'revenue'),
    bestPerformingMonth: _(processedData)
      .groupBy('month')
      .mapValues((sales) => _.sumBy(sales, 'revenue'))
      .toPairs()
      .maxBy(1),
    topCategory: _.maxBy(
      _.toPairs(categoryAnalysis),
      ([, data]) => data.totalRevenue
    )[0],
    topRegion: _.maxBy(
      _.toPairs(regionAnalysis),
      ([, data]) => data.totalRevenue
    )[0],
    topSalesperson: _.maxBy(
      _.toPairs(salespersonAnalysis),
      ([, data]) => data.totalRevenue
    )[0],
  };

  return {
    categoryAnalysis,
    regionAnalysis,
    salespersonAnalysis,
    timeSeriesAnalysis,
    productAnalysis,
    overallSummary,
  };
};

const analysisResults = analyzeSalesData(salesData);

console.log('カテゴリ別売上トップ3:');
_(analysisResults.categoryAnalysis)
  .toPairs()
  .orderBy(([, data]) => data.totalRevenue, 'desc')
  .take(3)
  .forEach(([category, data]) => {
    console.log(
      `${category}: ¥${data.totalRevenue.toLocaleString()} (${
        data.salesCount
      }件)`
    );
  });

console.log('\n営業担当者別パフォーマンス:');
_.forEach(
  analysisResults.salespersonAnalysis,
  (data, person) => {
    console.log(
      `${person}: ¥${data.totalRevenue.toLocaleString()} (平均: ¥${Math.round(
        data.averageSaleAmount
      ).toLocaleString()})`
    );
  }
);

複雑な売上データから多角的な分析結果を効率的に得ることができます。

39. ユーザー行動ログの分析

javascript// ユーザー行動ログデータ
const userActivityLogs = [
  {
    userId: 1,
    action: 'login',
    timestamp: '2023-12-01T09:00:00Z',
    page: '/dashboard',
    sessionId: 's1',
  },
  {
    userId: 1,
    action: 'view',
    timestamp: '2023-12-01T09:05:00Z',
    page: '/products',
    sessionId: 's1',
  },
  {
    userId: 1,
    action: 'click',
    timestamp: '2023-12-01T09:10:00Z',
    page: '/products',
    element: 'product-123',
    sessionId: 's1',
  },
  {
    userId: 1,
    action: 'view',
    timestamp: '2023-12-01T09:15:00Z',
    page: '/product/123',
    sessionId: 's1',
  },
  {
    userId: 1,
    action: 'purchase',
    timestamp: '2023-12-01T09:30:00Z',
    page: '/checkout',
    amount: 150,
    sessionId: 's1',
  },
  {
    userId: 1,
    action: 'logout',
    timestamp: '2023-12-01T09:35:00Z',
    page: '/dashboard',
    sessionId: 's1',
  },

  {
    userId: 2,
    action: 'login',
    timestamp: '2023-12-01T10:00:00Z',
    page: '/dashboard',
    sessionId: 's2',
  },
  {
    userId: 2,
    action: 'view',
    timestamp: '2023-12-01T10:05:00Z',
    page: '/products',
    sessionId: 's2',
  },
  {
    userId: 2,
    action: 'view',
    timestamp: '2023-12-01T10:10:00Z',
    page: '/about',
    sessionId: 's2',
  },
  {
    userId: 2,
    action: 'logout',
    timestamp: '2023-12-01T10:20:00Z',
    page: '/about',
    sessionId: 's2',
  },

  {
    userId: 1,
    action: 'login',
    timestamp: '2023-12-02T14:00:00Z',
    page: '/dashboard',
    sessionId: 's3',
  },
  {
    userId: 1,
    action: 'view',
    timestamp: '2023-12-02T14:05:00Z',
    page: '/account',
    sessionId: 's3',
  },
  {
    userId: 1,
    action: 'logout',
    timestamp: '2023-12-02T14:30:00Z',
    page: '/account',
    sessionId: 's3',
  },
];

// ユーザー行動分析
const analyzeUserActivity = (logs) => {
  // データの前処理
  const processedLogs = _.map(logs, (log) => ({
    ...log,
    timestamp: new Date(log.timestamp),
    date: new Date(log.timestamp)
      .toISOString()
      .split('T')[0],
    hour: new Date(log.timestamp).getHours(),
  }));

  // セッション分析
  const sessionAnalysis = _(processedLogs)
    .groupBy('sessionId')
    .mapValues((sessionLogs) => {
      const sortedLogs = _.sortBy(sessionLogs, 'timestamp');
      const startTime = _.first(sortedLogs).timestamp;
      const endTime = _.last(sortedLogs).timestamp;
      const duration = Math.round(
        (endTime - startTime) / 1000 / 60
      ); //

      return {
        userId: _.first(sortedLogs).userId,
        startTime,
        endTime,
        duration,
        pageViews: _.countBy(sessionLogs, 'page'),
        actions: _.countBy(sessionLogs, 'action'),
        totalActions: sessionLogs.length,
        pages: _.uniq(_.map(sessionLogs, 'page')),
        hadPurchase: _.some(sessionLogs, {
          action: 'purchase',
        }),
        purchaseAmount:
          _.sumBy(
            _.filter(sessionLogs, { action: 'purchase' }),
            'amount'
          ) || 0,
      };
    })
    .value();

  // ユーザー別行動パターン分析
  const userAnalysis = _(processedLogs)
    .groupBy('userId')
    .mapValues((userLogs) => {
      const sessions = _.uniqBy(userLogs, 'sessionId');
      const purchases = _.filter(userLogs, {
        action: 'purchase',
      });

      return {
        totalSessions: sessions.length,
        totalActions: userLogs.length,
        totalPurchases: purchases.length,
        totalSpent: _.sumBy(purchases, 'amount') || 0,
        averageSessionDuration: _.meanBy(
          sessions,
          (session) => {
            const sessionLogs = _.filter(userLogs, {
              sessionId: session.sessionId,
            });
            const sortedLogs = _.sortBy(
              sessionLogs,
              'timestamp'
            );
            return (
              (_.last(sortedLogs).timestamp -
                _.first(sortedLogs).timestamp) /
              1000 /
              60
            );
          }
        ),
        favoritePages: _(userLogs)
          .filter({ action: 'view' })
          .countBy('page')
          .toPairs()
          .orderBy(1, 'desc')
          .take(3)
          .map(0)
          .value(),
        mostActiveHour: _(userLogs)
          .countBy('hour')
          .toPairs()
          .maxBy(1)[0],
        conversionRate: purchases.length / sessions.length,
      };
    })
    .value();

  // ページ別パフォーマンス分析
  const pageAnalysis = _(processedLogs)
    .filter({ action: 'view' })
    .groupBy('page')
    .mapValues((pageViews) => {
      const sessions = _.uniqBy(pageViews, 'sessionId');
      const users = _.uniqBy(pageViews, 'userId');

      return {
        totalViews: pageViews.length,
        uniqueSessions: sessions.length,
        uniqueUsers: users.length,
        averageViewsPerSession:
          pageViews.length / sessions.length,
        bounceRate:
          _.filter(sessions, (session) => {
            const sessionLogs = _.filter(processedLogs, {
              sessionId: session.sessionId,
            });
            return sessionLogs.length === 1; // 1ページのみ閲覧
          }).length / sessions.length,
        leadsToPurchase: _(sessions)
          .map((session) => {
            const sessionLogs = _.filter(processedLogs, {
              sessionId: session.sessionId,
            });
            return _.some(sessionLogs, {
              action: 'purchase',
            });
          })
          .filter(Boolean)
          .size(),
      };
    })
    .value();

  // 時間帯別アクティビティ分析
  const hourlyActivity = _(processedLogs)
    .groupBy('hour')
    .mapValues((hourlyLogs) => ({
      totalActions: hourlyLogs.length,
      uniqueUsers: _.uniqBy(hourlyLogs, 'userId').length,
      purchases: _.filter(hourlyLogs, {
        action: 'purchase',
      }).length,
      revenue:
        _.sumBy(
          _.filter(hourlyLogs, { action: 'purchase' }),
          'amount'
        ) || 0,
    }))
    .toPairs()
    .map(([hour, data]) => ({
      hour: parseInt(hour),
      ...data,
    }))
    .sortBy('hour')
    .value();

  // 日別トレンド分析
  const dailyTrends = _(processedLogs)
    .groupBy('date')
    .mapValues((dailyLogs) => ({
      totalActions: dailyLogs.length,
      uniqueUsers: _.uniqBy(dailyLogs, 'userId').length,
      uniqueSessions: _.uniqBy(dailyLogs, 'sessionId')
        .length,
      purchases: _.filter(dailyLogs, { action: 'purchase' })
        .length,
      revenue:
        _.sumBy(
          _.filter(dailyLogs, { action: 'purchase' }),
          'amount'
        ) || 0,
    }))
    .toPairs()
    .sortBy(0)
    .value();

  return {
    sessionAnalysis,
    userAnalysis,
    pageAnalysis,
    hourlyActivity,
    dailyTrends,
  };
};

const activityResults = analyzeUserActivity(
  userActivityLogs
);

console.log('ユーザー別サマリー:');
_.forEach(activityResults.userAnalysis, (data, userId) => {
  console.log(
    `ユーザー${userId}: セッション${
      data.totalSessions
    }回, 購入${
      data.totalPurchases
    }回, 合計¥${data.totalSpent.toLocaleString()}`
  );
});

console.log('\nページ別パフォーマンス:');
_(activityResults.pageAnalysis)
  .toPairs()
  .orderBy(([, data]) => data.totalViews, 'desc')
  .forEach(([page, data]) => {
    console.log(
      `${page}: ${data.totalViews}PV, 直帰率${Math.round(
        data.bounceRate * 100
      )}%`
    );
  });

console.log('\n時間帯別アクティビティ (売上上位):');
_(activityResults.hourlyActivity)
  .orderBy('revenue', 'desc')
  .take(3)
  .forEach((data) => {
    console.log(
      `${data.hour}時: ¥${data.revenue.toLocaleString()} (${
        data.purchases
      }件の購入)`
    );
  });

ユーザー行動ログから具体的なインサイトを効率的に抽出できます。

まとめ

本記事では、Lodash を使った 50 の実践的なレシピを通じて、JavaScript 開発における配列・オブジェクト操作の効率化方法をご紹介しました。

配列操作では、生成・初期化から変換・加工、検索・抽出、集計・統計まで、日常的な開発作業で必要になる基本的なパターンを網羅しました。特に _.map()_.filter()_.groupBy() などの関数は、データ処理の中核となる重要な機能です。

オブジェクト操作においては、プロパティの動的な生成や安全なアクセス、深いマージなど、複雑なデータ構造を扱う際に威力を発揮する機能を学びました。_.get()_.set()_.merge() といった関数により、エラーを回避しながら柔軟なオブジェクト操作が可能になります。

配列とオブジェクトの相互変換では、API レスポンスの加工やデータ形式の標準化で実際に使用されるパターンを解説しました。これらの技術により、異なるデータソースからの情報を統一的に扱えるようになります。

実践的な応用例では、API レスポンスの正規化、フォームデータの処理、データ分析など、実際の開発現場で頻繁に遭遇するシナリオに基づいたソリューションを提供しました。これらのレシピは、そのまま実プロジェクトで活用できる実用的なものです。

Lodash の真の価値は、コードの可読性と保守性の向上にあります。ネイティブの JavaScript でも同様の処理は可能ですが、Lodash を使うことで、より短く、理解しやすく、エラーの少ないコードを書くことができます。

これらのレシピを参考に、皆さんの開発プロジェクトでも Lodash を積極的に活用していただければと思います。効率的なデータ処理により、より良いユーザー体験の提供と開発生産性の向上を実現してください。

関連リンク