Ad – 728Γ—90
πŸ“¦ Data Structures

JavaScript Spread and Rest – The ... Operator

The three dots ... are one of the most useful additions in ES6. In two different contexts, they do opposite things: spread expands an iterable into individual elements, while rest collects multiple elements into a single value. Both are essential for writing clean, functional, and immutable JavaScript.

⏱️ 18 min read 🎯 Intermediate πŸ“… Updated 2026

Spread in Arrays

The spread operator expands an iterable (array, string, Set, Map) into individual elements wherever a list is expected.

JavaScript
const a = [1, 2, 3];
const b = [4, 5, 6];

// Merge arrays
const merged = [...a, ...b];
console.log(merged); // [1, 2, 3, 4, 5, 6]

// Copy (shallow)
const copy = [...a];
copy.push(99);
console.log(a);    // [1, 2, 3] β€” unchanged
console.log(copy); // [1, 2, 3, 99]

// Prepend / append
const withExtra = [0, ...a, 4];
console.log(withExtra); // [0, 1, 2, 3, 4]

// Pass array as function arguments
function sum(x, y, z) { return x + y + z; }
const nums = [10, 20, 30];
console.log(sum(...nums)); // 60
// Equivalent to: sum(nums[0], nums[1], nums[2])

// Spread a string into characters
const chars = [..."hello"];
console.log(chars); // ['h', 'e', 'l', 'l', 'o']

// Spread a Set into array (deduplication)
const unique = [...new Set([1, 2, 2, 3, 3])];
console.log(unique); // [1, 2, 3]

Spread in Objects

JavaScript
const defaults = { theme: 'light', lang: 'en', fontSize: 14 };
const userPrefs = { theme: 'dark', fontSize: 16 };

// Shallow copy
const copy = { ...defaults };
console.log(copy); // { theme: 'light', lang: 'en', fontSize: 14 }

// Merge – later properties win (override)
const config = { ...defaults, ...userPrefs };
console.log(config);
// { theme: 'dark', lang: 'en', fontSize: 16 }

// Add / override a single property
const updated = { ...defaults, lang: 'fr' };
console.log(updated);
// { theme: 'light', lang: 'fr', fontSize: 14 }

// Remove a property (spread + destructuring)
const { theme, ...withoutTheme } = defaults;
console.log(withoutTheme); // { lang: 'en', fontSize: 14 }

// Order matters β€” last spread wins
const a = { x: 1, y: 2 };
const b = { y: 10, z: 3 };
console.log({ ...a, ...b }); // { x: 1, y: 10, z: 3 }
console.log({ ...b, ...a }); // { y: 2, z: 3, x: 1 }
β–Ά Output
{ theme: 'light', lang: 'en', fontSize: 14 }
{ theme: 'dark', lang: 'en', fontSize: 16 }
{ theme: 'light', lang: 'fr', fontSize: 14 }
{ lang: 'en', fontSize: 14 }
{ x: 1, y: 10, z: 3 }
{ y: 2, z: 3, x: 1 }

Rest Parameters in Functions

Rest parameters use the same ... syntax but in function definitions. They collect remaining arguments into a real array.

JavaScript
// Rest parameter collects extra arguments
function sum(first, ...rest) {
  return rest.reduce((acc, n) => acc + n, first);
}
console.log(sum(1, 2, 3, 4, 5)); // 15
console.log(sum(10));             // 10

// Variadic log with prefix
function log(level, ...messages) {
  console.log(`[${level.toUpperCase()}]`, ...messages);
}
log('info', 'Server started', 'on port', 3000);
// [INFO] Server started on port 3000

// Rest vs arguments object (pre-ES6)
function oldWay() {
  // arguments is array-like, NOT a real array
  const arr = Array.from(arguments); // needed conversion
  return arr.reduce((a, b) => a + b, 0);
}
function newWay(...args) {
  // args is already a real Array β€” has map, filter, reduce, etc.
  return args.reduce((a, b) => a + b, 0);
}

// Rest must be the last parameter
function first(a, b, ...rest) { // OK
  console.log(a, b, rest);
}
// function wrong(...rest, last) {} // SyntaxError!

Rest in Destructuring

JavaScript
// Array destructuring with rest
const [first, second, ...remaining] = [1, 2, 3, 4, 5];
console.log(first, second); // 1 2
console.log(remaining);     // [3, 4, 5]

// Object destructuring with rest
const { id, name, ...extras } = { id: 1, name: 'Alice', role: 'admin', age: 28 };
console.log(id, name); // 1 Alice
console.log(extras);   // { role: 'admin', age: 28 }

// Practical: extract known fields, pass the rest along
function createUser({ name, email, ...profile }) {
  return { id: Date.now(), name, email, profile };
}
const user = createUser({ name: 'Bob', email: 'b@x.com', age: 25, bio: 'Dev' });
console.log(user.profile); // { age: 25, bio: 'Dev' }

Shallow vs Deep Copy Gotcha

πŸ’‘
Spread is Always Shallow

Both [...arr] and {...obj} copy only one level deep. Nested objects and arrays are still shared by reference. Mutating a nested property in the copy will mutate the original too.

JavaScript
const original = { a: 1, nested: { b: 2 } };
const shallowCopy = { ...original };

shallowCopy.a = 99;            // OK β€” primitive, independent
shallowCopy.nested.b = 99;     // mutates ORIGINAL.nested.b too!

console.log(original.a);        // 1 ← unchanged
console.log(original.nested.b); // 99 ← CHANGED (shared reference!)

// Solutions for deep copy:
// 1. structuredClone (modern, handles most types)
const deepCopy = structuredClone(original);
deepCopy.nested.b = 42;
console.log(original.nested.b); // 99 β€” truly independent

// 2. JSON round-trip (simple data only β€” no functions/dates/undefined)
const jsonCopy = JSON.parse(JSON.stringify(original));

// 3. Manual deep copy for nested arrays
const matrix = [[1, 2], [3, 4]];
const matrixCopy = matrix.map(row => [...row]);
matrixCopy[0][0] = 99;
console.log(matrix[0][0]); // 1 β€” unchanged

Practical Patterns

JavaScript
// 1. Immutable state updates (React pattern)
const state = { user: { name: 'Alice' }, count: 0 };

// Add to array immutably
const newState1 = {
  ...state,
  items: [...(state.items ?? []), { id: 3, label: 'New' }]
};

// Update nested property immutably
const newState2 = {
  ...state,
  user: { ...state.user, name: 'Alice Smith' }
};

// 2. Merge config objects with defaults
const defaultOptions = { timeout: 5000, retries: 3, cache: true };
function request(url, options = {}) {
  const opts = { ...defaultOptions, ...options };
  console.log(`Fetching ${url} with`, opts);
}
request('/api/data', { timeout: 10000, cache: false });
// { timeout: 10000, retries: 3, cache: false }

// 3. Clone and add properties
const baseUser = { role: 'user', createdAt: Date.now() };
const adminUser = { ...baseUser, name: 'Carol', role: 'admin' };

// 4. Spread with Math functions
const values = [3, 1, 4, 1, 5, 9, 2, 6];
console.log(Math.max(...values)); // 9
console.log(Math.min(...values)); // 1

// 5. Convert NodeList to Array
const divs = document.querySelectorAll('div');
const divsArray = [...divs]; // now has .map, .filter, etc.
SyntaxContextBehaviour
[...arr]ExpressionSpread: expands iterable into elements
{...obj}ExpressionSpread: copies own enumerable properties
fn(...arr)Call siteSpread: passes array elements as arguments
function f(...args)Parameter listRest: collects extra args into array
const [a, ...b] = arrDestructuringRest: collects remaining elements
const { x, ...y } = objDestructuringRest: collects remaining properties
πŸ’‘
Spread vs Object.assign

Object.assign(target, src) and { ...target, ...src } both perform shallow merges. Key differences: Object.assign mutates target and triggers setters; spread always creates a new object and copies enumerable own properties directly without invoking setters.

Interview Questions

  • What is the difference between spread and rest in JavaScript?
  • How does spread differ from Object.assign for object copying?
  • Why is spread considered a shallow copy? Give an example of the problem.
  • How would you merge two arrays without mutating either?
  • What is wrong with using arguments instead of rest parameters?

πŸ‹οΈ Practical Exercise

Write a function mergeDeep(target, ...sources) that deeply merges multiple source objects into the target without mutating the sources. Test it with objects that have nested properties at two levels. (Hint: check if values are plain objects before merging recursively.)

πŸ”₯ Challenge Exercise

Implement a Redux-style reducer using only spread and rest. The reducer should handle three action types: ADD_ITEM (adds to an items array), UPDATE_ITEM (updates matching item by id), and REMOVE_ITEM (removes matching item). Every operation must produce a new state without mutating the previous one.

Frequently Asked Questions

Can I spread a non-iterable object into an array?
No. Spreading into an array context requires an iterable (arrays, strings, Sets, Maps, generators). Plain objects are not iterable, so [...obj] throws a TypeError. Object spread {...obj} works because it copies own enumerable properties, not via iteration.
Does {...obj} copy prototype methods?
No. Object spread only copies own, enumerable, string-keyed properties. Prototype methods and non-enumerable properties are not included.
Can rest parameters replace the arguments object entirely?
Almost. The main difference is that arguments also captures parameters before the rest element, and it is not available in arrow functions. Rest parameters are preferred in modern code because they are real arrays with full array methods.
How do I pass all arguments from one function to another?
Use rest + spread together: function wrapper(...args) { return inner(...args); }. This perfectly forwards all arguments regardless of their number or type.
What is the difference between structuredClone and JSON round-trip for deep cloning?
structuredClone handles Dates, RegExp, Map, Set, circular references, ArrayBuffers, and more. JSON round-trip silently drops undefined, functions, and Symbols, and converts Dates to strings. Prefer structuredClone for modern environments.
Ad – 336Γ—280

πŸ“‹ Summary

  • Spread expands an iterable or object into individual elements/properties.
  • Use spread to copy arrays ([...arr]), merge arrays ([...a, ...b]), and pass arguments (fn(...arr)).
  • Use spread to copy objects ({...obj}) and merge with overrides ({...defaults, ...overrides}).
  • Rest collects multiple items into a single array or object.
  • Rest parameters (...args) replace the legacy arguments object with a real array.
  • Both spread and object spread are shallow β€” use structuredClone() for deep copies.
  • Order matters for object spread: later properties override earlier ones.