Skip to main content

Command Palette

Search for a command to run...

Spread vs Rest in JavaScript: The Same Dots, Two Different Jobs

Updated
7 min readView as Markdown

If you have been writing JavaScript for a while, you have probably seen those three dots ... everywhere. In one place they expand an array into pieces. In another place they scoop up leftover arguments into a bundle. Same syntax, completely opposite behavior.

That is the trick. The dots do not have a fixed personality. They change based on where you put them. Think of them like a kitchen tool. A whisk can mix eggs into a smooth batter, or it can whip air into cream. Same tool, different job depending on context.

Let us look at both jobs in detail.


The Spread Operator: Unpacking Things

Spread takes something that holds multiple values and spreads those values out, one by one, into wherever you need them.

Arrays: Making Copies and Combinations

The most common use is copying an array without mutating the original.

const fruits = ['apple', 'banana', 'orange'];
const fruitsCopy = [...fruits];

console.log(fruitsCopy); 
// ['apple', 'banana', 'orange']
console.log(fruitsCopy === fruits); 
// false (different array in memory)

This is not the same as const copy = fruits. That just creates another name pointing to the same basket. Spread creates a brand new basket with the same contents.

You can also merge arrays cleanly.

const frontEnd = ['React', 'Vue'];
const backEnd = ['Node', 'Django'];

const fullStack = [...frontEnd, ...backEnd];
console.log(fullStack);
// ['React', 'Vue', 'Node', 'Django']

No concat, no loops, no mess. You can even slip new items in between.

const withExtras = ['HTML', ...frontEnd, 'CSS', ...backEnd];
console.log(withExtras);
// ['HTML', 'React', 'Vue', 'CSS', 'Node', 'Django']

Objects: Shallow Copies and Overriding Properties

Spread works with objects too. You can clone an object or merge properties from multiple sources.

const user = { name: 'Ravi', role: 'developer' };
const details = { city: 'Bangalore', experience: 3 };

const profile = { ...user, ...details };
console.log(profile);
// { name: 'Ravi', role: 'developer', city: 'Bangalore', experience: 3 }

Here is where it gets useful. You can override specific properties while keeping everything else.

const updatedUser = { ...user, role: 'senior developer' };
console.log(updatedUser);
// { name: 'Ravi', role: 'senior developer' }

The order matters. If user and details both have a role property, the last one wins.

const a = { x: 1, y: 2 };
const b = { y: 99, z: 3 };

const merged = { ...a, ...b };
console.log(merged);
// { x: 1, y: 99, z: 3 }

y becomes 99 because b came last and overwrote it.

Passing Arrays as Individual Arguments

Before spread, if you had an array of numbers and wanted to find the maximum, you would do this awkward dance:

const numbers = [10, 5, 30, 2];

// Old way
const max = Math.max.apply(null, numbers);

Spread makes it readable.

const max = Math.max(...numbers);
console.log(max); // 30

Math.max does not accept an array. It wants individual numbers. Spread unpacks the array and hands them over one by one.

// What actually happens under the hood
Math.max(10, 5, 30, 2);

A Quick Warning About Shallow Copies

Spread only copies the top level. If your array contains objects, or your object contains nested objects, those inner references stay shared.

const team = [
  { name: 'Alice', skills: ['JS'] },
  { name: 'Bob', skills: ['Python'] }
];

const teamCopy = [...team];

teamCopy[0].skills.push('React');
console.log(team[0].skills);
// ['JS', 'React']  (original changed too!)

Both team and teamCopy point to the same inner objects. If you need a deep copy, spread alone is not enough. You would need something like structuredClone or a library.


The Rest Operator: Gathering Leftovers

If spread is about unpacking a suitcase, rest is about throwing loose items into a bag. It collects individual values into a single array.

In Function Parameters

The classic use case is when a function needs to accept any number of arguments.

function sum(...numbers) {
  return numbers.reduce((total, num) => total + num, 0);
}

console.log(sum(1, 2, 3));      // 6
console.log(sum(10, 20));       // 30
console.log(sum());             // 0

Inside sum, numbers is a real array. You can call .map, .filter, .reduce on it directly. This is a huge upgrade from the old arguments object, which looked like an array but was not one.

// Old way (don't do this)
function oldSum() {
  // arguments is not a real array
  return Array.from(arguments).reduce((a, b) => a + b, 0);
}

Rest also works when you want to separate some named arguments from the leftovers.

function createUser(name, email, ...preferences) {
  return {
    name,
    email,
    preferences  // an array of whatever else was passed
  };
}

const user = createUser('Ravi', 'ravi@example.com', 'darkMode', 'notifications', 'weeklyDigest');
console.log(user);
// {
//   name: 'Ravi',
//   email: 'ravi@example.com',
//   preferences: ['darkMode', 'notifications', 'weeklyDigest']
// }

The first two arguments get proper names. Everything else gets bundled into preferences. This is clean and self-documenting.

In Destructuring

Rest shines when you are pulling values out of an array or object and want to keep whatever is left.

With arrays:

const colors = ['red', 'green', 'blue', 'yellow', 'purple'];

const [primary, secondary, ...others] = colors;

console.log(primary);   // 'red'
console.log(secondary); // 'green'
console.log(others);    // ['blue', 'yellow', 'purple']

With objects:

const product = {
  id: 101,
  name: 'Wireless Mouse',
  price: 29.99,
  stock: 150,
  category: 'Electronics'
};

const { id, name, ...metadata } = product;

console.log(id);       // 101
console.log(name);     // 'Wireless Mouse'
console.log(metadata);
// { price: 29.99, stock: 150, category: 'Electronics' }

This pattern is incredibly useful when you want to remove specific properties before sending data to an API or a child component.

function sendToApi(product) {
  // Strip out internal fields, keep only what the API needs
  const { internalId, createdBy, ...apiPayload } = product;
  return fetch('/api/products', { body: JSON.stringify(apiPayload) });
}

The Visual Difference

Here is the simplest way to keep them straight in your head:

Spread vs Rest Diagram

Spread takes one container and breaks it into pieces. Rest takes many pieces and builds one container.


Key Differences at a Glance

Aspect Spread (...) Rest (...)
Direction Expands out Collects in
Where it lives On the right side of =, in function calls, in array/object literals On the left side of =, in function parameters, in destructuring
Input One iterable (array, string, object) Multiple individual values
Output Individual values A single array
Must be last No Yes, when destructuring

Practical Use Cases You Will Actually Use

1. Cloning and Updating State in React

const [user, setUser] = useState({ name: 'Ravi', age: 28 });

// Update just the age
setUser({ ...user, age: 29 });

2. Combining Configuration Objects

const defaults = { theme: 'light', notifications: true };
const userSettings = { theme: 'dark' };

const finalConfig = { ...defaults, ...userSettings };
// { theme: 'dark', notifications: true }

User settings override defaults without destroying untouched defaults.

3. Logging with Rest

function log(level, ...messages) {
  console[level](`[${new Date().toISOString()}]`, ...messages);
}

log('warn', 'Disk space low', 'Cleanup recommended');
// [2024-01-15T10:30:00.000Z] Disk space low Cleanup recommended

4. Removing Properties Without Mutating

function removePassword(user) {
  const { password, ...safeUser } = user;
  return safeUser;
}

5. Converting a NodeList to an Array

const buttons = document.querySelectorAll('button');
const buttonArray = [...buttons];

buttonArray.map(btn => btn.addEventListener('click', handleClick));

querySelectorAll returns a NodeList, not an array. Spread converts it instantly.


The One Rule to Remember

If the three dots are on the receiving end, collecting values into a variable, it is rest. If they are on the giving end, unpacking values into a new place, it is spread.

// Rest: collecting
const [first, ...rest] = array;

// Spread: expanding
const newArray = [...array];

That is it. Same three dots. Opposite jobs. Context tells you which one is working.

More from this blog