The ... Operator
The three dots (...) can be used as either the spread operator (expanding elements) or the rest operator (collecting elements), depending on context.
📤 Spread Operator
Array Spread
// Expand array into individual elements
let numbers = [1, 2, 3];
console.log(...numbers); // 1 2 3 (separate arguments)
console.log(numbers); // [1, 2, 3] (array)
// Copy array (shallow)
let original = [1, 2, 3];
let copy = [...original];
copy.push(4);
console.log(original); // [1, 2, 3]
console.log(copy); // [1, 2, 3, 4]
// Concatenate arrays
let arr1 = [1, 2];
let arr2 = [3, 4];
let combined = [...arr1, ...arr2];
console.log(combined); // [1, 2, 3, 4]
// Old way
let combined = arr1.concat(arr2);
// Insert elements
let middle = [3, 4];
let array = [1, 2, ...middle, 5, 6];
console.log(array); // [1, 2, 3, 4, 5, 6]
// Add elements to copy
let numbers = [2, 3, 4];
let withBoundaries = [1, ...numbers, 5];
// [1, 2, 3, 4, 5]
Function Arguments
// Pass array as separate arguments
function sum(a, b, c) {
return a + b + c;
}
let numbers = [1, 2, 3];
console.log(sum(...numbers)); // 6
// Old way
console.log(sum.apply(null, numbers)); // 6
// Math functions
let numbers = [5, 1, 9, 3, 7];
console.log(Math.max(...numbers)); // 9
console.log(Math.min(...numbers)); // 1
// Old way
console.log(Math.max.apply(null, numbers));
// Push multiple elements
let arr = [1, 2];
let toAdd = [3, 4, 5];
arr.push(...toAdd);
console.log(arr); // [1, 2, 3, 4, 5]
// Old way
Array.prototype.push.apply(arr, toAdd);
Object Spread
// Copy object (shallow, ES2018)
let original = { a: 1, b: 2 };
let copy = { ...original };
copy.c = 3;
console.log(original); // { a: 1, b: 2 }
console.log(copy); // { a: 1, b: 2, c: 3 }
// Old way
let copy = Object.assign({}, original);
// Merge objects
let obj1 = { a: 1, b: 2 };
let obj2 = { c: 3, d: 4 };
let merged = { ...obj1, ...obj2 };
console.log(merged); // { a: 1, b: 2, c: 3, d: 4 }
// Later properties override earlier ones
let obj1 = { a: 1, b: 2 };
let obj2 = { b: 3, c: 4 };
let merged = { ...obj1, ...obj2 };
console.log(merged); // { a: 1, b: 3, c: 4 }
// Add/override properties
let user = { name: 'John', age: 30 };
let updated = { ...user, age: 31, city: 'NYC' };
console.log(updated); // { name: 'John', age: 31, city: 'NYC' }
// Default properties
let defaults = { timeout: 3000, retries: 3 };
let config = { timeout: 5000 };
let settings = { ...defaults, ...config };
console.log(settings); // { timeout: 5000, retries: 3 }
String Spread
// Spread string into characters
let str = 'hello';
let chars = [...str];
console.log(chars); // ['h', 'e', 'l', 'l', 'o']
// Same as
let chars = str.split('');
// Unique characters
let unique = [...new Set('hello')];
console.log(unique); // ['h', 'e', 'l', 'o']
// Spread in array literal
let arr = ['a', ...'hello', 'b'];
console.log(arr); // ['a', 'h', 'e', 'l', 'l', 'o', 'b']
Spread with Iterables
// Set to array
let set = new Set([1, 2, 3]);
let arr = [...set];
console.log(arr); // [1, 2, 3]
// Map to array
let map = new Map([['a', 1], ['b', 2]]);
let entries = [...map];
console.log(entries); // [['a', 1], ['b', 2]]
// NodeList to array
let divs = document.querySelectorAll('div');
let divsArray = [...divs];
divsArray.forEach(div => console.log(div));
// Arguments to array
function test() {
let args = [...arguments];
console.log(args);
}
test(1, 2, 3); // [1, 2, 3]
📥 Rest Operator
Function Parameters
// Collect remaining arguments
function sum(...numbers) {
return numbers.reduce((a, b) => a + b, 0);
}
console.log(sum(1, 2, 3)); // 6
console.log(sum(1, 2, 3, 4, 5)); // 15
// Mix regular and rest parameters
function greet(greeting, ...names) {
return `${greeting} ${names.join(' and ')}!`;
}
console.log(greet('Hello', 'John')); // "Hello John!"
console.log(greet('Hi', 'John', 'Jane', 'Bob')); // "Hi John and Jane and Bob!"
// Rest must be last parameter
function example(a, b, ...rest) {
console.log(a); // First arg
console.log(b); // Second arg
console.log(rest); // Remaining args
}
example(1, 2, 3, 4, 5);
// 1
// 2
// [3, 4, 5]
// Replace arguments object
function oldWay() {
let args = Array.from(arguments);
return args.reduce((a, b) => a + b);
}
function newWay(...args) {
return args.reduce((a, b) => a + b);
}
Array Destructuring
// Collect remaining elements
let [first, ...rest] = [1, 2, 3, 4, 5];
console.log(first); // 1
console.log(rest); // [2, 3, 4, 5]
// Skip elements and collect
let [, second, ...remaining] = [1, 2, 3, 4];
console.log(second); // 2
console.log(remaining); // [3, 4]
// Head and tail pattern
let [head, ...tail] = [1, 2, 3];
console.log(head); // 1
console.log(tail); // [2, 3]
// Empty rest
let [a, b, ...rest] = [1, 2];
console.log(rest); // []
// Rest in middle? NO! Must be last
// let [first, ...middle, last] = [1, 2, 3, 4]; // SyntaxError
Object Destructuring
// Collect remaining properties (ES2018)
let { name, age, ...rest } = {
name: 'John',
age: 30,
city: 'NYC',
job: 'Developer'
};
console.log(name); // 'John'
console.log(age); // 30
console.log(rest); // { city: 'NYC', job: 'Developer' }
// Extract some properties, keep others
let { id, createdAt, ...userData } = {
id: 1,
name: 'John',
email: 'john@example.com',
createdAt: '2025-01-01'
};
console.log(userData);
// { name: 'John', email: 'john@example.com' }
// Nested with rest
let { name, address: { city }, ...other } = {
name: 'John',
age: 30,
address: {
city: 'NYC',
zip: '10001'
}
};
console.log(other);
// { age: 30, address: { city: 'NYC', zip: '10001' } }
🔄 Combining Spread and Rest
// Function that accepts rest, uses spread
function merge(...objects) {
return Object.assign({}, ...objects);
}
let result = merge({ a: 1 }, { b: 2 }, { c: 3 });
console.log(result); // { a: 1, b: 2, c: 3 }
// Or with spread
function merge(...objects) {
return { ...objects.reduce((acc, obj) => ({ ...acc, ...obj }), {}) };
}
// Destructure then spread
function updateUser(updates) {
let { id, ...userData } = updates;
return { ...userData, updatedAt: new Date() };
}
// Collect args, process, spread result
function calculate(...numbers) {
let doubled = numbers.map(n => n * 2);
return Math.max(...doubled);
}
console.log(calculate(1, 5, 3, 7)); // 14
// Rest in parameters, spread in call
function wrapper(...args) {
return originalFunction(...args);
}
// Spread to rest, rest to spread
function proxy(...args) {
console.log('Calling with:', ...args);
return target(...args);
}
💡 Practical Examples
Array Operations
// Remove duplicates
let numbers = [1, 2, 2, 3, 3, 4];
let unique = [...new Set(numbers)];
console.log(unique); // [1, 2, 3, 4]
// Merge arrays
let arr1 = [1, 2];
let arr2 = [3, 4];
let arr3 = [5, 6];
let merged = [...arr1, ...arr2, ...arr3];
// [1, 2, 3, 4, 5, 6]
// Insert at specific position
let array = [1, 2, 5, 6];
let toInsert = [3, 4];
array = [...array.slice(0, 2), ...toInsert, ...array.slice(2)];
// [1, 2, 3, 4, 5, 6]
// Flatten one level
let nested = [[1, 2], [3, 4], [5, 6]];
let flat = [].concat(...nested);
// [1, 2, 3, 4, 5, 6]
// Clone and modify
let original = [1, 2, 3];
let modified = [...original, 4, 5];
console.log(original); // [1, 2, 3]
console.log(modified); // [1, 2, 3, 4, 5]
// Split array
let [first, second, ...others] = [1, 2, 3, 4, 5];
// first: 1, second: 2, others: [3, 4, 5]
Object Operations
// Update object immutably
let user = { name: 'John', age: 30, city: 'NYC' };
let updated = { ...user, age: 31 };
// Remove properties
let { password, ...safe } = user;
console.log(safe); // user without password
// Conditional properties
let includeEmail = true;
let user = {
name: 'John',
age: 30,
...(includeEmail && { email: 'john@example.com' })
};
// Merge with priority
let defaults = { timeout: 3000, retries: 3, debug: false };
let userConfig = { timeout: 5000 };
let config = { ...defaults, ...userConfig };
// Nested update (careful - shallow copy)
let state = {
user: { name: 'John', age: 30 },
settings: { theme: 'dark' }
};
let newState = {
...state,
user: { ...state.user, age: 31 }
};
// Clone deeply nested (shallow per level)
let original = {
a: 1,
b: { c: 2, d: { e: 3 } }
};
let copy = {
...original,
b: {
...original.b,
d: { ...original.b.d }
}
};
Function Utilities
// Flexible sum function
function sum(...numbers) {
return numbers.reduce((a, b) => a + b, 0);
}
console.log(sum(1, 2)); // 3
console.log(sum(1, 2, 3, 4, 5)); // 15
// Logger with prefix
function log(prefix, ...messages) {
console.log(`[${prefix}]`, ...messages);
}
log('INFO', 'Server started', 'Port:', 3000);
// Apply function with context
function apply(fn, context, ...args) {
return fn.apply(context, args);
}
// Curry with rest
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn(...args);
}
return (...nextArgs) => curried(...args, ...nextArgs);
};
}
let add = (a, b, c) => a + b + c;
let curriedAdd = curry(add);
console.log(curriedAdd(1)(2)(3)); // 6
// Compose functions
function compose(...fns) {
return x => fns.reduceRight((v, fn) => fn(v), x);
}
let addOne = x => x + 1;
let double = x => x * 2;
let square = x => x * x;
let compute = compose(square, double, addOne);
console.log(compute(2)); // 36
React/State Management
// Update state immutably
let state = {
users: [{ id: 1, name: 'John' }],
settings: { theme: 'dark' }
};
// Add user
let newState = {
...state,
users: [...state.users, { id: 2, name: 'Jane' }]
};
// Update user
let newState = {
...state,
users: state.users.map(user =>
user.id === 1 ? { ...user, name: 'John Doe' } : user
)
};
// Remove user
let newState = {
...state,
users: state.users.filter(user => user.id !== 1)
};
// Update nested property
let newState = {
...state,
settings: { ...state.settings, theme: 'light' }
};
// Redux reducer pattern
function reducer(state = initialState, action) {
switch (action.type) {
case 'ADD_TODO':
return {
...state,
todos: [...state.todos, action.payload]
};
case 'UPDATE_TODO':
return {
...state,
todos: state.todos.map(todo =>
todo.id === action.payload.id
? { ...todo, ...action.payload }
: todo
)
};
default:
return state;
}
}
API and Data Handling
// Merge API responses
async function loadDashboard() {
let [users, posts, stats] = await Promise.all([
fetch('/api/users').then(r => r.json()),
fetch('/api/posts').then(r => r.json()),
fetch('/api/stats').then(r => r.json())
]);
return { users, posts, stats };
}
// Combine pagination results
async function loadAllPages(baseUrl) {
let allData = [];
let page = 1;
while (true) {
let { data, hasMore } = await fetch(`${baseUrl}?page=${page}`)
.then(r => r.json());
allData = [...allData, ...data];
if (!hasMore) break;
page++;
}
return allData;
}
// Transform and filter data
function processUsers(users, filters) {
let { minAge, ...otherFilters } = filters;
return users
.filter(user => user.age >= minAge)
.map(({ password, ...safe }) => safe)
.map(user => ({ ...user, ...otherFilters }));
}
// Partial update
function updateResource(id, updates) {
return fetch(`/api/resources/${id}`, {
method: 'PATCH',
body: JSON.stringify(updates),
headers: { 'Content-Type': 'application/json' }
});
}
let { name, email } = formData;
updateResource(userId, { name, email });
DOM Manipulation
// NodeList to array
let elements = [...document.querySelectorAll('.item')];
elements.forEach(el => el.classList.add('active'));
// Convert HTMLCollection
let divs = [...document.getElementsByTagName('div')];
// Combine element arrays
let headers = [...document.querySelectorAll('h1, h2, h3')];
// Clone attributes
let element = document.querySelector('.original');
let clone = document.createElement('div');
[...element.attributes].forEach(attr => {
clone.setAttribute(attr.name, attr.value);
});
// Get all text nodes
function getAllTextNodes(element) {
let walker = document.createTreeWalker(
element,
NodeFilter.SHOW_TEXT
);
let nodes = [];
while (walker.nextNode()) {
nodes = [...nodes, walker.currentNode];
}
return nodes;
}
⚠️ Common Pitfalls
// Shallow copy issue
let original = { a: 1, b: { c: 2 } };
let copy = { ...original };
copy.b.c = 3;
console.log(original.b.c); // 3 (mutated!)
// Solution: deep clone nested objects
let copy = {
...original,
b: { ...original.b }
};
// Or use structuredClone (modern)
let copy = structuredClone(original);
// Spread order matters
let obj = { a: 1, b: 2, c: 3 };
let result1 = { ...obj, b: 99 }; // { a: 1, b: 99, c: 3 }
let result2 = { b: 99, ...obj }; // { a: 1, b: 2, c: 3 }
// Rest must be last
// function bad(...rest, last) {} // SyntaxError
function good(first, ...rest) {} // OK
// Can't spread non-iterables
// let obj = { ...42 }; // Error
let obj = { ...{ a: 1 } }; // OK
let arr = [...[1, 2, 3]]; // OK
let arr = [...'hello']; // OK
// Spread in object only copies own properties
let obj = Object.create({ inherited: 1 });
obj.own = 2;
let copy = { ...obj };
console.log(copy); // { own: 2 } (no inherited)
🎯 Key Takeaways
- Spread (...): Expands iterables into individual elements
- Rest (...): Collects multiple elements into array/object
- Array Copy: [...arr] creates shallow copy
- Object Copy: {...obj} creates shallow copy (ES2018)
- Merge Arrays: [...arr1, ...arr2] concatenates arrays
- Merge Objects: {...obj1, ...obj2} later props override
- Function Args: ...args collects all arguments into array
- Shallow Copies: Nested objects/arrays are not deep cloned