Skip to main content

Command Palette

Search for a command to run...

Mastering Spread and Rest Operators in JavaScript

Learn the one mental model that separates spread from rest — with practical patterns, real-world exercises, and the pitfalls every JavaScript developer should know.

Updated
Mastering Spread and Rest Operators in JavaScript
M

👋 Hey, I'm Mohd Kaif – a student documenting my journey through code. I write about what I'm learning in real-time – the wins, the struggles, and the "aha!" moments. From JavaScript and React to backend systems with Node.js, databases, DevOps, TypeScript, and AI integrations. This blog is my public learning journal: honest, evolving, and always exploring. If you're curious about any of these topics, let's learn and build together!

Most people don't realize they've been using two completely different operators — written the exact same way.

You've typed ... dozens of times. Maybe in a function parameter, maybe while copying an array, maybe while merging two objects in a React component. It worked, so you moved on.

But here's what trips up even experienced developers:

  • Why does ...args in a function collect values, but ...arr in a function call expands them?

  • Why does spread sometimes copy an object and sometimes overwrite it?

  • Why does the rest operator throw a syntax error when it's not last?

If any of that sounds familiar, you're not missing some fundamental skill — you're missing one mental model that ties it all together. And once you have it, both operators click into place immediately.


✅ What You'll Learn

  • How the spread operator unpacks arrays and objects — and why it matters for immutability

  • How the rest operator collects values — and why it's the foundation of flexible APIs

  • When to reach for each one (with a rule you can apply instantly)

  • Why they look identical but behave oppositely — including what JavaScript does under the hood

  • What common mistakes to avoid (the shallow copy trap has caught everyone at least once)

  • How to combine both in real-world patterns like state updates, function wrappers, and data sanitization

No prerequisites required. If you know basic JavaScript arrays and functions, you're ready.


The One Mental Model That Changes Everything

Before diving into syntax, burn this into your brain:

Spread  →  takes ONE thing, breaks it into MANY
Rest    →  takes MANY things, groups them into ONE

Or think of it like packing and unpacking a suitcase:

  • Spread is unpacking — you open the suitcase and lay every item out on the bed

  • Rest is packing — you gather loose items and stuff them into the suitcase

Same ... syntax. Opposite direction. The only thing that changes is where you write it.


Part 1: The Spread Operator — "Unpack This"

The spread operator takes an iterable — an array, string, or object — and expands it into individual elements. Think of it as hitting "unzip."

Spreading into a function call

const nums = [10, 20, 30];

console.log(Math.max(...nums));
// 30

Without spread, Math.max([10, 20, 30]) returns NaN — it doesn't accept an array. Spread unpacks the array into three separate arguments, exactly what Math.max expects.

Spreading arrays

Combining two arrays:

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

const stack = [...frontend, ...backend];
// ['React', 'Vue', 'Node', 'Django']

Making a true copy (not a reference):

const original = [1, 2, 3];
const copy = [...original];

copy.push(4);

console.log(original); // [1, 2, 3]  ✅ untouched
console.log(copy);     // [1, 2, 3, 4]

This is a pattern you'll use constantly in React and Redux — any time you need to update state without mutating the original.

Spreading objects

Merging two objects:

const user    = { name: 'Priya' };
const account = { plan: 'pro', active: true };

const profile = { ...user, ...account };
// { name: 'Priya', plan: 'pro', active: true }

Overriding specific properties:

const defaultConfig = { theme: 'light', fontSize: 14, sidebar: true };

// User wants dark mode — override just that one field
const userConfig = { ...defaultConfig, theme: 'dark' };
// { theme: 'dark', fontSize: 14, sidebar: true }

Key rule: When spreading objects, the last one wins. If two spreads share a key, the later one takes priority. This is why { ...defaults, ...overrides } is such a common config pattern.


⚡ Exercise 1: Merge Without Mutation

You have two objects and a rule: you cannot modify either original.

const serverSettings = { timeout: 5000, retries: 3, verbose: false };
const userOverrides  = { timeout: 10000, verbose: true };

// Goal: create finalSettings where userOverrides win
// Expected: { timeout: 10000, retries: 3, verbose: true }

Try it before looking at the solution below.

Solution

const finalSettings = { ...serverSettings, ...userOverrides };
// { timeout: 10000, retries: 3, verbose: true }

The key insight: spreading serverSettings first lays down all defaults, then userOverrides overwrites just the keys it has — leaving retries: 3 untouched.


Part 2: The Rest Operator — "Collect the Rest"

The rest operator does the opposite: it gathers a group of individual values into a single array. It shows up in two places — function parameters and destructuring.

Rest in function parameters

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

sum(1, 2, 3, 4, 5);
// 15

Here, ...numbers collects every argument passed to sum into a single array. You can call it with 2 arguments or 200 — the function handles both.

Rest with named parameters

You don't have to collect all arguments. You can name the first few and collect the remainder:

function greet(greeting, ...names) {
  return names.map(name => `\({greeting}, \){name}!`);
}

greet('Hello', 'Arjun', 'Sofia', 'Lena');
// ['Hello, Arjun!', 'Hello, Sofia!', 'Hello, Lena!']

The first argument binds to greeting. Everything after goes into names. Clean, readable, and no arguments object needed.

Rest in array destructuring

const [first, second, ...remaining] = [10, 20, 30, 40, 50];

console.log(first);     // 10
console.log(second);    // 20
console.log(remaining); // [30, 40, 50]

This is perfect for processing lists where you care about the head but want to pass the tail along unchanged.

Rest in object destructuring

const user = {
  id: 42,
  name: 'Marcus',
  password: 'hunter2',
  email: 'marcus@example.com'
};

const { password, ...safeUser } = user;

console.log(safeUser);
// { id: 42, name: 'Marcus', email: 'marcus@example.com' }

This pattern — destructure out what you don't want, keep the rest — is one of the cleanest ways to sanitize objects before sending them to a client or logging them.


⚡ Exercise 2: Build a Variadic Logger

Write a function log that:

  • Takes a level as its first argument ('info', 'warn', or 'error')

  • Collects any number of additional messages

  • Prints: [LEVEL] message1 message2 ...

log('info', 'Server started', 'Port 3000');
// [INFO] Server started Port 3000

log('error', 'Database connection failed');
// [ERROR] Database connection failed

Solution

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

Notice the double use of ...: rest collects messages in the parameters, then spread unpacks them back out in the console.log call. Both operators in 2 lines.


Part 3: Side by Side — What's Actually Different

Here's the clearest way to see the distinction:

Spread Rest
Direction One → Many Many → One
Where it appears Function calls, array/object literals Function parameters, destructuring
What it takes An iterable (array, string, Set…) Individual values
What it produces Individual elements An array
Mental model Unzip Zip up

Same line, different role

function process(...items) {      // ← REST: collects args into array
  return doSomething(...items);   // ← SPREAD: expands array into args
}

The variable items is an array. Inside the function, rest packed it. Then spread unpacked it again to pass to doSomething. Both operators, same variable, one line apart.


Part 4: Under the Hood — Why This Works

This isn't magic. Both operators are built on JavaScript's iterator protocol.

Any object that implements [Symbol.iterator]() is iterable. Arrays, strings, Sets, Maps, and generator functions are all iterable. That's why spread works on all of them:

const str = 'hello';
console.log([...str]);
// ['h', 'e', 'l', 'l', 'o']

const set = new Set([1, 2, 3]);
console.log([...set]);
// [1, 2, 3]

When the JavaScript engine sees ...someValue in an expansion position (function call or array literal), it calls someValue[Symbol.iterator]() and steps through each value one at a time — like pulling items one by one off a conveyor belt.

Object spread is the exception. Plain objects {} are not iterable — they don't have Symbol.iterator. So {...obj} uses Object.assign-style enumeration instead, copying own enumerable properties. That's also why you can't spread a plain object into an array:

const obj = { a: 1 };
console.log([...obj]); // ❌ TypeError: obj is not iterable

Rest, meanwhile, isn't about the iterator protocol at all — it's purely a syntax feature. The parser sees ...rest in a parameter or destructuring pattern and generates code to collect remaining values into a new array at that position.

This is exactly why rest must always come last:

// ❌ SyntaxError — parser can't know when "rest" ends
function fn(...first, last) {}

// ❌ SyntaxError — same reason
const [...head, tail] = arr;

// ✅ Works — rest is always the remainder
function fn(first, ...rest) {}

Part 5: Real-World Patterns You'll Actually Use

1. Immutable state updates (React / Redux)

// Updating a nested field without mutation
const state = {
  user: { name: 'Elena', role: 'admin' },
  settings: { theme: 'dark' }
};

const newState = {
  ...state,
  user: { ...state.user, role: 'viewer' }  // only role changes
};

The original state object is untouched. This is the correct pattern for reducers.

2. Function wrappers with pass-through arguments

function withLogging(fn, ...args) {
  console.time(fn.name);
  const result = fn(...args);  // spread args back out
  console.timeEnd(fn.name);
  return result;
}

withLogging(fetchUser, userId, options);

Rest collects. Spread re-forwards. The wrapper doesn't need to know how many arguments fn takes.

3. Sanitizing API responses

async function getPublicProfile(userId) {
  const user = await db.users.findById(userId);
  const { passwordHash, internalNotes, stripeId, ...publicData } = user;
  return publicData;  // safe to send to client
}

Destructure out the sensitive fields by name, return the rest. Clean, explicit, and self-documenting.

4. Combining configuration sources

const defaults   = { retries: 3, timeout: 5000, debug: false };
const envConfig  = loadFromEnv();    // may override some fields
const userConfig = loadFromFile();   // highest priority

const config = { ...defaults, ...envConfig, ...userConfig };

Later spreads win. Cascade from least to most specific.

5. Array deduplication pipeline

function mergeUnique(...arrays) {
  return [...new Set(arrays.flat())];
}

mergeUnique([1, 2, 3], [2, 3, 4], [3, 4, 5]);
// [1, 2, 3, 4, 5]

⚡ Exercise 3: The Omit Utility

Write a function omit(obj, ...keys) that returns a new object with the specified keys removed. No mutation allowed.

const product = { id: 1, name: 'Laptop', internalSku: 'LP-001', cost: 800, price: 1299 };

omit(product, 'internalSku', 'cost');
// { id: 1, name: 'Laptop', price: 1299 }

Solution

function omit(obj, ...keys) {
  return Object.fromEntries(
    Object.entries(obj).filter(([key]) => !keys.includes(key))
  );
}

Rest collects the keys to remove. Object.entries + filter + Object.fromEntries handles the rest — no mutation, no libraries.


Part 6: Mistakes That Will Catch You (And How to Avoid Them)

Mistake 1: Thinking spread does a deep copy

const obj = { user: { name: 'Alex', scores: [10, 20] } };
const copy = { ...obj };

copy.user.name = 'Sam';

console.log(obj.user.name); // 'Sam' ← original is affected ❗

Spread only copies the top-level properties. Nested objects are still shared references. If you need a true deep clone, use structuredClone(obj) (available in all modern environments) or a library like lodash.cloneDeep.

const deepCopy = structuredClone(obj); // ✅ fully independent

Mistake 2: Assuming order doesn't matter in object spread

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

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

The right side wins. Always think about which object should take priority when keys conflict.

Mistake 3: Spreading a non-iterable into an array

const obj = { a: 1 };
const arr = [...obj]; // ❌ TypeError: obj is not iterable

Plain objects don't have Symbol.iterator. Spread them inside {}, not [].

Mistake 4: Rest in the wrong position

// ❌ SyntaxError every time
const [a, ...middle, z] = [1, 2, 3, 4];
function fn(first, ...rest, last) {}

Rest is always last. It means "everything remaining" — there can't be anything after "everything."


Quick Reference: Which One Do I Use?

Ask yourself: "Am I expanding or collecting?"

Situation Use
Calling a function with array elements Spread
Copying an array or object Spread
Merging multiple objects Spread
Overriding specific properties Spread
Accepting variable number of arguments Rest
Separating head from tail in a list Rest
Removing specific keys from an object Rest
Forwarding unknown arguments Both (rest to collect, spread to pass)

What to Learn Next

You've got spread and rest solid. Here's where to go from here:

  1. Destructuring in depth — rest is just one piece of destructuring. Default values, renaming, and nested patterns open up much cleaner function signatures and data handling.

  2. Array methods: map, filter, reduce — these methods pair naturally with spread to build immutable data transformation pipelines you'll use in every codebase.

  3. The iterator protocol — you saw a glimpse here with Symbol.iterator. Understanding iterators unlocks custom iterables, generators, and how for...of actually works.

  4. Immutability patterns in React — now that you understand spread, you're ready to dive into how React's state model is built around it, and how tools like Immer make deep updates easier.


💬 Got Questions?

Drop a comment below — I'd love to hear which of these patterns you've used, which caught you off guard, or any edge cases you've run into.

Here are topics I'm covering in upcoming articles:

  • Destructuring mastery: Default values, renaming, nested patterns, and function parameter destructuring

  • Array methods deep dive: How map, filter, and reduce work under the hood — with real-world chaining examples

  • The iterator protocol explained: Writing your own iterables, generators, and understanding for...of

  • Immutability in practice: Working with complex state in React without accidental mutations


Found this helpful? Share it with a developer who's still wrestling with .... And if something didn't click, leave a comment — I read every one.

Happy coding. 🚀

Zero to Full Stack Developer: From Basics to Production

Part 25 of 50

Complete full-stack web development series from zero to production. Learn HTML, CSS, JavaScript, TypeScript, React, Next.js, Node.js, databases, Docker, AWS, and AI integration. Build real-world projects step-by-step.

Up next

JavaScript Destructuring Explained

A practical ES6 guide to arrays, objects, default values, and real-world patterns — with hands-on exercises.