JavaScript Spread Operatorの実践的な使い方|実務で使えるサンプルコード集

JavaScript

JavaScript Spread Operatorの実践的な使い方|実務で使えるサンプルコード集

はじめに

JavaScriptの開発をしていると、配列やオブジェクトの操作は日常的に行われます。その際に頻出するのが「spread operator(スプレッド演算子)」です。ES2015で導入されたこの機能は、現代的なJavaScript開発において必須のテクニックとなっています。

本記事では、教科書的な説明ではなく、実務プロジェクトで実際に使われているパターンに焦点を当てて解説します。単純な配列展開から、複雑なデータ変換まで、実装レベルで役立つサンプルコードを豊富に用意しました。

Spread Operatorの基本解説

Spread operatorは、配列やオブジェクトを展開して個別の要素として扱う機能です。記号は「…」(ドット3つ)で表記されます。

基本的な役割は以下の3つです:

  • 配列要素の展開
  • オブジェクトプロパティの展開
  • 関数の引数への展開

シンプルな例を見てみましょう:

// 配列の展開
const arr1 = [1, 2, 3];
const arr2 = [...arr1, 4, 5];
console.log(arr2); // [1, 2, 3, 4, 5]

// オブジェクトの展開
const obj1 = { name: 'Taro', age: 30 };
const obj2 = { ...obj1, city: 'Tokyo' };
console.log(obj2); // { name: 'Taro', age: 30, city: 'Tokyo' }

これは非常にシンプルですが、実務では更に複雑な場面で活躍します。

実務での主要ユースケース

1. REST APIレスポンスの処理

最も一般的なユースケースは、サーバーから取得したデータを加工する場面です。API応答から不要なフィールドを削除したり、新しいデータを追加したりする処理は頻繁に行われます。

以下は、ユーザー情報を取得して、フロントエンドで使いやすい形に変換する実装例です:

// API から取得したユーザーデータ
const apiResponse = {
  id: 12345,
  name: 'Taro Yamada',
  email: 'taro@example.com',
  internal_id: 'usr_xxx',
  created_timestamp: 1234567890,
  updated_timestamp: 1234567891,
  admin_notes: 'VIP customer',
  status: 'active'
};

// 必要なフィールドだけを抽出して新しいオブジェクトを作成
function formatUserData(apiUser) {
  const { id, name, email, status } = apiUser;
  return {
    id,
    name,
    email,
    status,
    lastUpdated: new Date().toISOString()
  };
}

const formattedUser = formatUserData(apiResponse);
console.log(formattedUser);
// { id: 12345, name: 'Taro Yamada', email: 'taro@example.com', status: 'active', lastUpdated: '...' }

2. 複数オブジェクトのマージ

実務では、複数の情報源からデータを集めてマージすることが多いです。例えば、ユーザーの基本情報、プロフィール情報、設定情報を組み合わせるような場面です。

// ユーザーの各種データソース
const basicInfo = {
  id: 1,
  name: 'Taro',
  email: 'taro@example.com'
};

const profileData = {
  bio: 'Software Engineer',
  avatar: 'https://example.com/avatar.jpg',
  followers: 150
};

const settings = {
  theme: 'dark',
  notifications: true,
  language: 'ja'
};

// すべてを統合
const completeUserData = {
  ...basicInfo,
  ...profileData,
  ...settings
};

console.log(completeUserData);
// {
//   id: 1,
//   name: 'Taro',
//   email: 'taro@example.com',
//   bio: 'Software Engineer',
//   avatar: 'https://example.com/avatar.jpg',
//   followers: 150,
//   theme: 'dark',
//   notifications: true,
//   language: 'ja'
// }

3. 配列操作での不変性保証

React や Vue などのフレームワークでは、状態の不変性が重要です。spread operator を使えば、元の配列を変更せずに新しい配列を作成できます。

// 元の配列
const tasks = [
  { id: 1, title: 'Design mockup', completed: false },
  { id: 2, title: 'Code review', completed: false },
  { id: 3, title: 'Deploy', completed: true }
];

// タスクを完了としてマーク(新しい配列を作成)
function completeTask(tasks, taskId) {
  return tasks.map(task => 
    task.id === taskId 
      ? { ...task, completed: true }
      : task
  );
}

const updatedTasks = completeTask(tasks, 2);
console.log(updatedTasks);
// 元の tasks 配列は変更されていない
console.log(tasks === updatedTasks); // false

実装コード例:実務プロジェクト

プロジェクト例:ECサイトの商品管理システム

実際の業務に近い、ECサイトの商品管理システムの実装を見てみましょう。

// 商品の初期データ
const productDatabase = [
  { id: 1, name: 'Laptop', price: 1200, stock: 5, category: 'Electronics' },
  { id: 2, name: 'Mouse', price: 25, stock: 50, category: 'Accessories' },
  { id: 3, name: 'Keyboard', price: 80, stock: 30, category: 'Accessories' }
];

// 商品を追加する関数
function addProduct(products, newProduct) {
  return [
    ...products,
    {
      id: products.length + 1,
      ...newProduct,
      createdAt: new Date().toISOString(),
      reviews: []
    }
  ];
}

// 商品の在庫を更新する関数
function updateStock(products, productId, newStock) {
  return products.map(product => 
    product.id === productId
      ? { ...product, stock: newStock }
      : product
  );
}

// 複数の商品に対して価格割引を適用
function applyDiscount(products, discount) {
  return products.map(product => ({
    ...product,
    originalPrice: product.price,
    price: Math.floor(product.price * (1 - discount)),
    discounted: true
  }));
}

// 使用例
const withNewProduct = addProduct(productDatabase, {
  name: 'Monitor',
  price: 300,
  stock: 15,
  category: 'Electronics'
});

const withUpdatedStock = updateStock(withNewProduct, 2, 45);
const discounted = applyDiscount(withUpdatedStock, 0.1); // 10%割引

console.log(discounted);

TypeScript での型安全な実装

実務プロジェクトではTypeScriptを使うことが多いでしょう。spread operatorとTypeScriptを組み合わせた実装例です:

interface User {
  id: number;
  name: string;
  email: string;
  age?: number;
}

interface UserProfile extends User {
  bio: string;
  avatar: string;
  followers: number;
}

// 基本ユーザー情報からプロフィール情報を作成
function createUserProfile(user: User, profile: Omit): UserProfile {
  return {
    ...user,
    ...profile
  };
}

// 使用例
const basicUser: User = {
  id: 1,
  name: 'Taro',
  email: 'taro@example.com'
};

const fullProfile = createUserProfile(basicUser, {
  bio: 'Software Engineer',
  avatar: 'https://example.com/avatar.jpg',
  followers: 200
});

console.log(fullProfile);

よくある応用パターン

パターン1: 条件付きプロパティの追加

特定の条件下でのみプロパティを追加したい場合があります。

function createUserObject(name, email, isPremium = false) {
  return {
    name,
    email,
    createdAt: new Date(),
    ...(isPremium && {
      premiumFeatures: ['ad-free', 'priority-support', 'analytics'],
      subscriptionEnd: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000)
    })
  };
}

const regularUser = createUserObject('Taro', 'taro@example.com', false);
const premiumUser = createUserObject('Hanako', 'hanako@example.com', true);

console.log(regularUser);
// { name: 'Taro', email: 'taro@example.com', createdAt: Date }

console.log(premiumUser);
// { name: 'Hanako', email: 'hanako@example.com', createdAt: Date, premiumFeatures: [...], subscriptionEnd: Date }

パターン2: ネストされたオブジェクトの更新

深くネストされたオブジェクトを更新する際は、各階層でspread operatorを使う必要があります。

const companyData = {
  name: 'Tech Company',
  departments: [
    {
      id: 1,
      name: 'Engineering',
      employees: [
        { id: 101, name: 'Taro', position: 'Senior Engineer' },
        { id: 102, name: 'Hanako', position: 'Engineer' }
      ]
    },
    {
      id: 2,
      name: 'Sales',
      employees: [
        { id: 201, name: 'Jiro', position: 'Sales Manager' }
      ]
    }
  ]
};

// 特定の従業員の職位を更新
function promoteEmployee(company, deptId, employeeId, newPosition) {
  return {
    ...company,
    departments: company.departments.map(dept => 
      dept.id === deptId
        ? {
            ...dept,
            employees: dept.employees.map(emp =>
              emp.id === employeeId
                ? { ...emp, position: newPosition }
                : emp
            )
          }
        : dept
    )
  };
}

const updated = promoteEmployee(companyData, 1, 102, 'Senior Engineer');
console.log(updated.departments[0].employees[1].position); // 'Senior Engineer'

パターン3: REST パラメータとの組み合わせ

関数定義でREST パラメータとして使用することで、可変長の引数を受け取れます。

// 複数のオブジェクトをマージする汎用関数
function mergeObjects(...objects) {
  return objects.reduce((acc, obj) => ({ ...acc, ...obj }), {});
}

const obj1 = { a: 1, b: 2 };
const obj2 = { c: 3, d: 4 };
const obj3 = { e: 5, f: 6 };

const merged = mergeObjects(obj1, obj2, obj3);
console.log(merged); // { a: 1, b: 2, c: 3, d: 4, e: 5, f: 6 }

// ユーザーに複数のタグを追加
function addTagsToUser(user, ...newTags) {
  return {
    ...user,
    tags: [...(user.tags || []), ...newTags]
  };
}

const user = { id: 1, name: 'Taro', tags: ['developer'] };
const updatedUser = addTagsToUser(user, 'javascript', 'react', 'nodejs');
console.log(updatedUser); 
// { id: 1, name: 'Taro', tags: ['developer', 'javascript', 'react', 'nodejs'] }

Python での同等の実装

参考までに、同じロジックをPythonで実装する場合を示します。Pythonでも辞書やリストの展開を行えます。

from datetime import datetime
from typing import List, Dict, Any

# ユーザーオブジェクトのマージ
def merge_user_data(basic_info: Dict[str, Any], profile_data: Dict[str, Any]) -> Dict[str, Any]:
    return {
        **basic_info,
        **profile_data
    }

basic_info = {'id': 1, 'name': 'Taro', 'email': 'taro@example.com'}
profile_data = {'bio': 'Engineer', 'avatar': 'https://example.com/avatar.jpg'}

result = merge_user_data(basic_info, profile_data)
print(result)
# {'id': 1, 'name': 'Taro', 'email': 'taro@example.com', 'bio': 'Engineer', 'avatar': 'https://example.com/avatar.jpg'}

# リストの結合
def add_product(products: List[Dict], new_product: Dict) -> List[Dict]:
    return [*products, {**new_product, 'created_at': datetime.now().isoformat()}]

products = [
    {'id': 1, 'name': 'Laptop', 'price': 1200},
    {'id': 2, 'name': 'Mouse', 'price': 25}
]

new_product = {'id': 3, 'name': 'Keyboard', 'price': 80}
with_new = add_product(products, new_product)
print(with_new)

# 複数の辞書をマージ
def merge_objects(*objects: Dict) -> Dict:
    result = {}
    for obj in objects:
        result = {**result, **obj}
    return result

obj1 = {'a': 1, 'b': 2}
obj2 = {'c': 3, 'd': 4}
obj3 = {'e': 5}

merged = merge_objects(obj1, obj2, obj3)
print(merged)  # {'a': 1, 'b': 2, 'c': 3, 'd': 4, 'e': 5}

注意点と落とし穴

1. シャローコピーであること

spread operatorでコピーされるのはシャロー(浅い)コピーです。ネストされたオブジェクトや配列は参照がコピーされるだけで、新しいオブジェクトは作成されません。

const original = {
  user: { name: 'Taro', age: 30 },
  tags: ['javascript', 'react']
};

const copied = { ...original };

// ネストされたオブジェクトは参照が同じ
console.log(original.user === copied.user); // true

// 更新すると両方に影響
copied.user.age = 31;
console.log(original.user.age); // 31 になってしまう

// 対策:深いコピーが必要な場合
const deepCopied = {
  ...original,
  user: { ...original.user },
  tags: [...original.tags]
};

console.log(original.user === deepCopied.user); // false
deepCopied.user.age = 32;
console.log(original.user.age); // 31 のまま

2. プロパティの上書き順序

複数のオブジェクトをマージする際、同じキーが存在すると後に指定されたものが優先されます。意図しない上書きに注意が必要です。

const config1 = { apiUrl: 'https://api.example.com', timeout: 5000 };
const config2 = { timeout: 10000, retries: 3 };

// config2 のtimeout が優先される
const merged = { ...config1, ...config2 };
console.log(merged); 
// { apiUrl: 'https://api.example.com', timeout: 10000, retries: 3 }

// 意図的に優先順位を制御
const userConfig = { timeout: 20000 };
const finalConfig = { ...config1, ...config2, ...userConfig };
console.log(finalConfig.timeout); // 20000

3. パフォーマンスの考慮

大規模な配列やオブジェクトを繰り返しコピーする場合、パフォーマンスへの影響を考慮する必要があります。

// ❌ 悪い例:ループ内で毎回配列をコピー
function badApproach(items) {
  let result = [];
  for (let item of items) {
    result = [...result, item]; // 毎回配列全体をコピー
  }
  return result;
}

// ✅ 良い例:push メソッドを使用
function goodApproach(items) {
  let result = [];
  for (let item of items) {
    result.push(item); // O(1) の操作
  }
  return result;
}

// ✅ または map を使用
function betterApproach(items) {
  return items.map(item => item);
}

4. undefined や null の扱い

条件付きプロパティを追加する際、falsy な値でも展開されないように注意が必要です。

const user = {
  name: 'Taro',
  // age が undefined の場合、展開しない
  ...(undefined && { age: undefined })
};

console.log(user); // { name: 'Taro' }

// null も同様
const user2 = {
  name: 'Taro',
  ...(null && { extra: 'data' })
};

console.log(user2); // { name: 'Taro' }

// ただし空の文字列や 0 の場合は注意
const user3 = {
  name: 'Taro',
  ...(0 && { count: 0 })  // 0 は falsy なので展開されない
};

console.log(user3); // { name: 'Taro' }

実務での注意点まとめ

  • シャローコピー:ネストされたオブジェクトは参照がコピーされるだけ
  • 上書き順序:後に指定されたプロパティが優先される
  • パフォーマンス:ループ内での繰り返しコピーは避ける
  • 型安全性:TypeScript使用時は型定義に注意
  • 可読性:複雑な場合は細分化や関数化を検討

実務で使えるベストプラクティス

以下は実際のプロジェクトで役立つベストプラクティスです:

// ✅ 推奨:明確な関数に分ける
function updateUserEmail(user, newEmail) {
  return {
    ...user,
    email: newEmail,
    emailVerified: false,
    lastEmailChangeAt: new Date()
  };
}

// ✅ 推奨:複雑な場合はビルダーパターン
class UserBuilder {
  constructor(user) {
    this.user = user;
  }
  
  withEmail(email) {
    return new UserBuilder({ ...this.user, email });
  }
  
  withProfile(profile) {
    return new UserBuilder({ ...this.user, ...profile });
  }
  
  build() {
    return this.user;
  }
}

const user = new UserBuilder({ id: 1, name: 'Taro' })
  .withEmail('taro@example.com')
  .withProfile({ bio: 'Engineer', avatar: 'https://...' })
  .build();

// ✅ 推奨:デフォルト値との組み合わせ
function createConfig(userConfig = {}) {
  const defaults = {
    apiUrl: 'https://api.example.com',
    timeout: 5000,
    retries: 3
  };
  
  return { ...defaults, ...userConfig };
}

const config = createConfig({ timeout: 10000 });
console.log(config);
// { apiUrl: 'https://api.example.com', timeout: 10000, retries: 3 }

まとめ

JavaScriptのspread operatorは、モダンな開発において欠かせない機能です。本記事で紹介した実務パターンは、実際のプロジェクトで頻繁に登場するケースばかりです。

重要なポイントを整理すると:

  • 配列・オブジェクトの展開:簡潔で読みやすいコード
  • 不変性の実現:フレームワークとの相性が良い
  • データ変換:API レスポンスの処理に最適
  • シンプルな記法:複雑な操作も明確に表現できる

最初は基本的な使い方から始めて、徐々に複雑なパターンに応用していくことをお勧めします。TypeScript を使用する場合は、型定義と組み合わせることで、さらに安全で保守しやすいコードになります。

実務での開発を効率化するために、ここで紹介したパターンやベストプラクティスをぜひ活用してください。

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