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 を使用する場合は、型定義と組み合わせることで、さらに安全で保守しやすいコードになります。
実務での開発を効率化するために、ここで紹介したパターンやベストプラクティスをぜひ活用してください。

