Spread & Rest Operators

Difficulty: Intermediate

The three dots (...) in JavaScript serve two distinct purposes depending on context: the spread operator and the rest parameter. Despite sharing the same syntax, they do opposite things. Spread expands an iterable (array, string, object) into individual elements, while rest collects multiple individual elements into a single array or object. Understanding when JavaScript interprets ... as spread versus rest is a fundamental skill.

The spread operator in arrays lets you copy arrays, merge multiple arrays, and convert iterables (like strings or Sets) into arrays. When you write [...arr], you get a shallow copy of arr. When you write [...arr1, ...arr2], you merge both arrays into a new one. You can also insert elements at any position: [...arr1, newItem, ...arr2]. This is the idiomatic way to work with arrays immutably in JavaScript - instead of push, splice, or direct mutation, you create new arrays with spread.

Object spread works the same way but with properties. { ...obj } creates a shallow copy, and { ...obj1, ...obj2 } merges objects with later properties overriding earlier ones. This is the standard pattern for immutable state updates in React and Redux. You can override specific properties by placing them after the spread: { ...user, name: 'New Name' } copies all of user's properties but replaces the name. Just like Object.assign, spread only does a shallow copy - nested objects are still shared references.

Rest parameters in function definitions collect all remaining arguments into a true array. Unlike the old arguments object (which is array-like but not an actual array), rest parameters give you a proper Array with full access to map, filter, reduce, and all other array methods. Rest parameters must be the last parameter in the function signature. They are also used in destructuring assignments to gather the remaining elements or properties.

The key mental model is: spread appears on the right side of an assignment or in function call arguments (it breaks things apart), while rest appears on the left side of an assignment or in function parameter definitions (it gathers things together). Once you internalize this distinction, the three dots become one of the most natural parts of modern JavaScript syntax.

Code examples

Spread in arrays - copy, merge, and convert

const frontend = ['React', 'Vue', 'Angular'];
const backend = ['Node', 'Django', 'Rails'];

// Shallow copy
const frontendCopy = [...frontend];
frontendCopy.push('Svelte');
console.log(frontend.length);   // original unchanged
console.log(frontendCopy.length);

// Merge arrays
const fullStack = [...frontend, ...backend];
console.log(fullStack);

// Convert string to array of characters
const chars = [...'hello'];
console.log(chars);

// Convert Set to array (deduplicate)
const nums = [1, 2, 2, 3, 3, 3];
const unique = [...new Set(nums)];
console.log(unique);

Spread creates a new array, so pushing to frontendCopy does not affect frontend. Merging with spread is cleaner than concat. Spreading a string splits it into characters. The Set + spread pattern is the cleanest way to deduplicate an array.

Spread in objects - immutable updates

const user = { name: 'Alice', age: 28, role: 'dev' };

// Shallow copy
const copy = { ...user };
console.log(copy);

// Override specific properties
const updated = { ...user, age: 29, role: 'senior dev' };
console.log(updated);
console.log(user); // original unchanged

// Merge multiple objects (last wins)
const defaults = { theme: 'light', lang: 'en', notify: true };
const prefs = { theme: 'dark' };
const config = { ...defaults, ...prefs };
console.log(config);

Object spread copies all own enumerable properties. Properties listed after the spread override matching keys from the spread source. This is the standard pattern for immutable state updates - copy everything, then override what changed.

Rest parameters in functions

// Rest collects remaining arguments into a real array
function sum(label, ...numbers) {
  const total = numbers.reduce((acc, n) => acc + n, 0);
  return `${label}: ${total}`;
}

console.log(sum('Total', 10, 20, 30, 40));
console.log(sum('Score', 95));

// Compare with arguments object (old way)
function oldSum() {
  // arguments is array-like, not a real array
  console.log(Array.isArray(arguments)); 
  return Array.from(arguments).reduce((a, b) => a + b, 0);
}

function newSum(...args) {
  console.log(Array.isArray(args)); // true - it's a real array
  return args.reduce((a, b) => a + b, 0);
}

console.log(oldSum(1, 2, 3));
console.log(newSum(1, 2, 3));

Rest parameters collect all arguments after 'label' into a proper array called 'numbers'. Unlike the legacy arguments object (which is array-like but not a real Array), rest parameters give you a genuine Array with all its methods available directly.

Spread vs rest - side by side

// SPREAD: expands elements (right side / arguments)
const parts = [3, 4, 5];
const all = [1, 2, ...parts, 6]; // spread expands parts
console.log(all);

Math.max(...parts); // spread in function call
console.log(Math.max(...parts));

// REST: collects elements (left side / parameters)
const [first, ...others] = all;  // rest collects remaining
console.log(first);
console.log(others);

// Object: spread to remove a property (rest to exclude)
const { password, ...safeUser } = {
  name: 'Alice',
  email: 'alice@dev.io',
  password: 'secret123'
};
console.log(safeUser);

Spread expands parts into individual values inside the array literal and the Math.max call. Rest collects the remaining array elements after first into the 'others' array. The last example is a powerful pattern - destructure out the property you want to remove, and rest collects everything else into a clean object without the password.

Key points

Concepts covered

spread operator, rest parameters, array spread, object spread, shallow copy, merge, rest in destructuring, spread vs rest