Skip to main content

Command Palette

Search for a command to run...

Spread vs Rest Operators in JavaScript

Published
5 min read
Spread vs Rest Operators in JavaScript
S
Full-stack developer obsessed with performance, scalability, and clean systems. I use Arch btw.

Introduction

JavaScript's Spread & Rest Operator look identical. Both are three dots (...). But they do opposite things depending on where you put them.

One expands values out. The other collects values in. Once that clicks, the rest is just syntax.


What the Spread Operator Does

The spread operator takes something iterable, like an array or object, and unpacks its contents into individual elements.

Think of it like opening a box and laying everything out on the table.

const fruits = ['apple', 'banana', 'mango'];
console.log(...fruits); // apple banana mango

That's useful, but the real power shows up when you're building new arrays or objects.

const first = [1, 2, 3];
const second = [4, 5, 6];
const combined = [...first, ...second];
// [1, 2, 3, 4, 5, 6]

No concat. No loops. You just spread both arrays into a new one.


What the Rest Operator Does

Rest does the reverse. Instead of expanding values, it collects multiple values into a single array.

It's most useful in function parameters when you don't know how many arguments are coming in.

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

sum(1, 2, 3);        // 6
sum(10, 20, 30, 40); // 100

Here, ...numbers gathers every argument into an array. You're not unpacking anything. You're bundling things up.


The Core Difference

Spread Rest
What it does Expands an iterable into individual values Collects individual values into an array
Where it's used Array/object literals, function calls Function parameter lists
Direction Out (unpacking) In (bundling)

A quick way to remember: if the ... is on the right side of an assignment or inside a function call, it's spread. If it's in the function definition's parameter list, it's rest.


Using Spread with Arrays

Copying an array

const original = [1, 2, 3];
const copy = [...original];
copy.push(4);

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

This gives you a shallow copy. Mutating copy won't touch original. That said, if the array contains objects, those objects are still shared by reference.

Inserting values while merging

const start = [1, 2];
const end = [5, 6];
const full = [...start, 3, 4, ...end];
// [1, 2, 3, 4, 5, 6]

You can drop values in between spreads. That's something concat can't do as cleanly.

Passing array items as function arguments

const nums = [3, 1, 4, 1, 5];
const max = Math.max(...nums); // 5

Math.max expects individual arguments, not an array. Spread bridges that gap without needing .apply().


Using Spread with Objects

Spread works on objects too. It copies key-value pairs into a new object.

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

const settings = { ...defaults, ...userPrefs };
// { theme: 'light', lang: 'fr', fontSize: 14 }

When keys overlap, the last one wins. Here, lang from userPrefs overwrites the one from defaults.

This pattern shows up constantly in config merging, Redux reducers, and anywhere you're building objects from multiple sources.


Practical Use Cases

Removing a property from an object

const user = { id: 1, name: 'Sara', password: 'secret' };
const { password, ...safeUser } = user;

console.log(safeUser); // { id: 1, name: 'Sara' }

Here rest is used in destructuring. You pull out the property you don't want, and everything else goes into safeUser.

Handling variable function arguments

function log(level, ...messages) {
    messages.forEach(msg => console.log(`[${level}]`, msg));
}

log('INFO', 'Server started', 'Listening on port 3000');
// [INFO] Server started
// [INFO] Listening on port 3000

The first argument is captured separately. Everything after goes into the rest array.

Adding or overriding properties without mutation

const state = { count: 0, loading: false };

const nextState = { ...state, count: state.count + 1 };
// { count: 1, loading: false }

You get a new object. The original is untouched. This is the standard way to handle state updates in React without mutating directly.

Cloning and extending arrays

const base = ['read', 'write'];
const extended = [...base, 'execute'];
// ['read', 'write', 'execute']

Useful when building permission sets or option lists that inherit from a base.


A Note on Shallow Copies

Both spread and rest create shallow copies. For arrays and objects of primitives, that's fine. But if your data is nested, you'll need something deeper, like structuredClone, for full isolation.

const nested = { a: { b: 1 } };
const copy = { ...nested };

copy.a.b = 99;
console.log(nested.a.b); // 99 (shared reference)

This trips people up. When you need a fully independent copy, use structuredClone. It recursively clones the entire structure, so nothing is shared.

const nested = { a: { b: 1 } };
const clone = structuredClone(nested);

clone.a.b = 99;
console.log(nested.a.b); // 1 (untouched)

structuredClone is built into modern browsers and Node.js 17+, so no library needed. For deeply nested data, it's the cleaner option over spread.


Conclusion

The same three dots, two different jobs. Spread unpacks. Rest collects. Once you see that pattern, you'll start noticing it everywhere, and using it without thinking twice.