Spread and Rest Operators
Learn the JavaScript spread and rest operators with real examples. Clone arrays, merge objects, write variadic functions, and master the everyday patterns used in modern JavaScript and React.
In the last article on destructuring in JavaScript, we learned how to unpack arrays and objects into clean, usable variables. If you haven’t gone through that one yet, I’d recommend you do, because we built some solid ground there that today’s lesson directly builds on.
Near the end of that article, there was a pattern that looked something like this:
const [first, ...remaining] = [10, 20, 30, 40];
console.log(first); // 10
console.log(remaining); // [20, 30, 40]
Those three dots. You probably noticed them. And if you’re curious about what they actually are and where else they show up, you’re in exactly the right place.
Today we’re covering the spread operator and the rest parameter, both use ..., same three dots, but they pull in completely opposite directions. One expands things out. The other collects things in. Once that distinction clicks, everything else about them becomes obvious.
Let’s work through it properly.
01Same Three Dots, Two Completely Different Jobs
Before we go anywhere, here’s the mental model you need. Hold onto this:
- Spread: Takes one thing (like an array or object) and expands it into individual pieces.
- Rest: Takes individual pieces and collects them into one thing (an array).
They’re opposites. Spread breaks apart. Rest brings together.
And here’s the one rule that tells you which one you’re looking at: check where in the code those three dots appear.
- Dots in a function call, or inside an array or object literal: that’s spread.
- Dots in a function’s parameter list: that’s a rest parameter.
That’s the whole trick. Context is everything.
Now let’s dig into each one with real examples.
02Part One: The Spread Operator and What It Can Do
Passing Array Items as Function Arguments
Here’s a situation you’ll run into: you have an array of numbers, and you want to find the largest one using Math.max().
The catch is that Math.max() doesn’t accept an array. It expects individual numbers, like Math.max(5, 2, 9). So how do you work with an array?
Before ES6, people had to use .apply():
const scores = [88, 42, 95, 67, 100];
// Before spread (the old way)
const highest = Math.max.apply(null, scores);
console.log(highest); // 100
It works, but it’s awkward. You have to pass null as the first argument, which feels weird. Nobody really liked this.
With the JavaScript spread operator, it becomes something you can actually read:
const scores = [88, 42, 95, 67, 100];
// With spread
const highest = Math.max(...scores);
console.log(highest); // 100
That ...scores tells JavaScript: “unpack every item in this array and pass them as individual arguments.” It’s the same as writing Math.max(88, 42, 95, 67, 100) by hand, but obviously much better when your array has dynamic length.
You can also mix spread with regular arguments:
const baseScores = [88, 42, 95];
// Add extra values alongside the spread
const highest = Math.max(100, ...baseScores, 77);
console.log(highest); // 100
This flexibility is what makes the spread operator genuinely useful. You’re not locked into any specific placement.
Cloning Arrays Without Sharing a Reference
If you read the JavaScript data types article, you know that arrays are objects, and objects are stored by reference. That means doing this doesn’t actually create a copy:
const original = ['a', 'b', 'c'];
const copy = original; // This is NOT a copy
copy.push('d');
console.log(original); // ['a', 'b', 'c', 'd'] ← original changed!
console.log(copy); // ['a', 'b', 'c', 'd']
Both variables point to the same array in memory. Modify one, you modify both. This causes real bugs in real projects.
The spread operator solves this cleanly:
const original = ['a', 'b', 'c'];
const copy = [...original]; // A genuine, independent clone
copy.push('d');
console.log(original); // ['a', 'b', 'c'] ← untouched
console.log(copy); // ['a', 'b', 'c', 'd']
[...original] creates a brand new array, takes all the items from original, and places them in it. Two separate arrays in memory. No shared reference.
This is one of the most-used patterns in React and any state management code, because you always want to work with copies rather than mutating the original data directly.
Merging Arrays in JavaScript
Combining two arrays used to be a .concat() job:
const teamA = ['Alice', 'Bob'];
const teamB = ['Carol', 'Dan'];
const allPlayers = teamA.concat(teamB);
console.log(allPlayers); // ['Alice', 'Bob', 'Carol', 'Dan']
The spread version is more readable and, more importantly, more flexible because you can insert extra items anywhere in the mix:
const teamA = ['Alice', 'Bob'];
const teamB = ['Carol', 'Dan'];
// Merge both teams, and add a new player in between
const allPlayers = [...teamA, 'Eve', ...teamB];
console.log(allPlayers); // ['Alice', 'Bob', 'Eve', 'Carol', 'Dan']
You can spread as many arrays as you want in a single literal, in whatever order makes sense. Each one “pours” its contents into the new array at that position.
Quick note: spread works on any iterable, not just arrays. You can spread strings, Sets, Maps, and even DOM NodeLists:
// Spreading a string
const chars = [..."hello"];
console.log(chars); // ['h', 'e', 'l', 'l', 'o']
// Spreading a Set (removes duplicates, then spreads)
const unique = [...new Set([1, 2, 2, 3, 3, 3])];
console.log(unique); // [1, 2, 3]
That second example is actually a really clean pattern for removing duplicates from an array. You’ll see it everywhere in real projects.
Array Spread Playground
Interactive
Enter comma-separated values for both arrays and try the operations below.
03Part Two: Spreading Objects
Array spread was introduced in ES6, and it was great. Then, a couple of years later with ES2018, the same idea landed for objects. And honestly, object spread might be even more useful in day-to-day work.
Cloning an Object with the Spread Operator
Same reference problem as arrays, different shape. When you do const copy = original with an object, you’re just copying the reference. Both variables point to the same object.
const user = { name: 'Sara', age: 28 };
const copy = user; // NOT a clone
copy.age = 99;
console.log(user.age); // 99 ← original changed
Spread gives you a proper clone:
const user = { name: 'Sara', age: 28 };
const copy = { ...user }; // Real clone
copy.age = 99;
console.log(user.age); // 28 ← untouched
console.log(copy.age); // 99
The { ...user } syntax says: “create a new object and copy every key-value pair from user into it.” Independent object, no shared reference.
Merging Two Objects into One
This is where the ES6 spread operator for objects really shines. Suppose you have a default settings object and a user’s custom settings, and you want to combine them:
const defaults = {
theme: 'dark',
fontSize: 14,
notifications: true
};
const userPrefs = {
fontSize: 18,
language: 'en'
};
const settings = { ...defaults, ...userPrefs };
console.log(settings);
// { theme: 'dark', fontSize: 18, notifications: true, language: 'en' }
See what happened there: the final object has all the keys from defaults, but fontSize got overridden by the value from userPrefs, because userPrefs came second. The language key got added because it only exists in userPrefs.
Order Matters More Than You Think
When you merge objects with spread, the later object wins on any conflicting keys. This isn’t random. It’s intentional and predictable:
const obj1 = { color: 'red', size: 'small' };
const obj2 = { color: 'blue', weight: 'light' };
// obj2 comes second, so its color wins
const merged = { ...obj1, ...obj2 };
console.log(merged); // { color: 'blue', size: 'small', weight: 'light' }
// Flip the order, obj1's color wins
const mergedFlipped = { ...obj2, ...obj1 };
console.log(mergedFlipped); // { color: 'red', weight: 'light', size: 'small' }
You can use this to your advantage all the time. Put the “defaults” first, then the “overrides” after. Whatever comes last takes priority on shared keys.
You can also add or update individual properties directly in the spread expression:
const product = { id: 1, name: 'Laptop', price: 999 };
// Update just the price, keep everything else
const updated = { ...product, price: 849 };
console.log(updated); // { id: 1, name: 'Laptop', price: 849 }
This is the core pattern behind immutable state updates in React and Redux. Instead of modifying the original object directly, you create a new object with the changed value. Stable, predictable, traceable.
Object Merge Explorer
Interactive
See how spread merges objects and which property wins when keys overlap. Change the order to watch priorities flip.
04Part Three: The Rest Parameter
Now let’s flip direction. Instead of spreading things out, we’re going to collect them in.
The rest parameter lets you write functions that accept any number of arguments. You define it in the function’s parameter list with ..., and JavaScript bundles all the remaining arguments into a real array for you.
Writing Variadic Functions in JavaScript
A variadic function is just a function that can take a flexible number of arguments. Think of how console.log() accepts one argument or ten. That’s variadic behavior.
Here’s a simple example: a function that adds up however many numbers you pass it:
function sum(...numbers) {
return numbers.reduce((total, n) => total + n, 0);
}
console.log(sum(1, 2)); // 3
console.log(sum(1, 2, 3, 4)); // 10
console.log(sum(10, 20, 30)); // 60
The ...numbers inside the parameter list is the rest parameter. Whatever arguments you pass to sum(), they all get collected into an array called numbers. Then you can do anything with that array, loop it, filter it, reduce it, whatever you need.
You can also mix named parameters with the rest parameter. Put the named ones first, rest at the end:
function introduce(firstName, lastName, ...skills) {
console.log(`Name: ${firstName} ${lastName}`);
console.log(`Skills: ${skills.join(', ')}`);
}
introduce('Maria', 'Garcia', 'JavaScript', 'React', 'CSS');
// Name: Maria Garcia
// Skills: JavaScript, React, CSS
introduce('James', 'Chen');
// Name: James Chen
// Skills: (empty string, skills is [])
firstName gets “Maria”, lastName gets “Garcia”, and everything else goes into the skills array. When you only pass two arguments, skills is just an empty array. No errors, no undefined weirdness.
One rule you must follow: the rest parameter must be the last parameter in the list. There can only be one, and nothing else can come after it. This won’t work:
// This will throw a SyntaxError
function broken(...args, lastOne) { } // ❌ Rest must be last
// This is correct
function correct(first, ...rest) { } // ✅
Rest Parameter vs the arguments Object
If you’ve looked at older JavaScript code, you might have come across something called arguments. It’s a special array-like object available inside every non-arrow function that contains all passed arguments:
function oldStyle() {
console.log(arguments); // Arguments object (not a real array)
console.log(arguments[0]); // First argument
console.log(arguments.length); // Count of arguments
}
oldStyle('a', 'b', 'c');
// Arguments { 0: 'a', 1: 'b', 2: 'c', length: 3 }
It works, but arguments has some real problems. First, it’s not a real array. You can’t call .map(), .filter(), or .reduce() on it directly. You’d have to convert it first. Second, it doesn’t exist inside arrow functions at all. Third, it captures every argument, so you can’t have named params alongside it.
The rest parameter fixes all of that:
// With the arguments object (old way)
function oldSum() {
return Array.from(arguments).reduce((a, b) => a + b, 0); // Has to convert first
}
// With rest parameter (modern way)
function newSum(...nums) {
return nums.reduce((a, b) => a + b, 0); // Already a real array
}
console.log(oldSum(1, 2, 3)); // 6
console.log(newSum(1, 2, 3)); // 6
nums is a genuine array from the start. You call array methods on it directly. Cleaner, more predictable, and it works inside arrow functions too.
Also worth noting: the arguments object is still technically available in non-arrow functions, but in modern JavaScript, rest parameters are the preferred way to handle this. You’ll see it everywhere in new codebases.
Rest in Destructuring: Where the Previous Article Connects
Remember that snippet from the start? This is where it lives:
// Array destructuring with rest
const [first, second, ...remaining] = [10, 20, 30, 40, 50];
console.log(first); // 10
console.log(second); // 20
console.log(remaining); // [30, 40, 50]
// Object destructuring with rest
const { name, age, ...otherDetails } = {
name: 'Alex',
age: 30,
city: 'Tokyo',
job: 'Developer'
};
console.log(name); // 'Alex'
console.log(age); // 30
console.log(otherDetails); // { city: 'Tokyo', job: 'Developer' }
In both cases, the three dots say “grab everything that’s left and put it in this variable.” That’s the rest behavior: collecting the remainder.
If this destructuring syntax looks unfamiliar, the destructuring article breaks it all down from scratch. These two concepts pair together really naturally.
Rest Parameter Live Demo
Interactive
Enter comma-separated arguments below and see how the rest parameter maps each one.
05Spread vs Rest: One Clear Way to Tell Them Apart
Let’s bring this together with a side-by-side look, because this is the comparison that makes everything stick:
// ─────────────────────────────────────────────
// SPREAD: in a function CALL or array/object LITERAL
// It EXPANDS something into pieces
// ─────────────────────────────────────────────
const nums = [3, 7, 1, 9];
Math.max(...nums); // Spread in a function call
const clone = [...nums]; // Spread in an array literal
const obj = { ...other }; // Spread in an object literal
// ─────────────────────────────────────────────
// REST: in a function PARAMETER list
// It COLLECTS pieces into one array
// ─────────────────────────────────────────────
function sum(...values) { // Rest in parameter list
return values.reduce((a, b) => a + b, 0);
}
const [head, ...tail] = nums; // Rest in destructuring
The position of the three dots in your code tells you everything. When you’re creating something (a call, a literal), it’s spreading out. When you’re receiving something (a parameter), it’s collecting in.
Read the dots from the perspective of the code around them, and you’ll never mix them up.
06Real Patterns You’ll Use Every Single Day
Config Objects with Safe Defaults
This is one of the most common patterns in any library or API:
const defaultConfig = {
timeout: 5000,
retries: 3,
method: 'GET',
headers: {}
};
function fetchData(url, userConfig = {}) {
const config = { ...defaultConfig, ...userConfig };
console.log(config);
// Makes the actual fetch with merged config...
}
fetchData('/api/users', { timeout: 10000, method: 'POST' });
// { timeout: 10000, retries: 3, method: 'POST', headers: {} }
User provides only what they want to override. Everything else comes from the defaults. Clean, readable, and the user doesn’t have to pass the full config every time.
Immutable State Updates (the React Way)
In React, you never mutate state directly. You always create a new version of it. Spread makes this natural:
const state = {
user: { name: 'Jamie', loggedIn: false },
count: 0,
loading: false
};
// Update just the loading property
const loadingState = { ...state, loading: true };
// Update a nested property (note: requires careful handling)
const loggedInState = {
...state,
user: { ...state.user, loggedIn: true }
};
console.log(state.loading); // false ← original unchanged
console.log(loadingState.loading); // true
console.log(loggedInState.user); // { name: 'Jamie', loggedIn: true }
Every “update” creates a fresh object. The original stays intact. This is the foundation of predictable state management in modern JavaScript applications.
Collecting Leftover Properties
When you receive an object but only need part of it, rest in destructuring lets you extract what you need and bundle the rest:
function displayUser({ name, avatar, ...metadata }) {
// Use name and avatar to render the UI
console.log(`Showing profile for: ${name}`);
console.log(`Avatar URL: ${avatar}`);
// metadata has everything else: age, bio, location, etc.
console.log('Additional info:', metadata);
}
displayUser({
name: 'Priya',
avatar: 'https://example.com/avatar.jpg',
age: 26,
bio: 'Front-end developer',
location: 'Mumbai'
});
// Showing profile for: Priya
// Avatar URL: https://example.com/avatar.jpg
// Additional info: { age: 26, bio: 'Front-end developer', location: 'Mumbai' }
This is great for separating “display” properties from “data” properties without throwing anything away. The function parameter is itself a destructuring pattern with a rest inside it. Powerful combination.
Spreading Arguments Through a Chain of Functions
function greet(greeting, name, punctuation) {
return `${greeting}, ${name}${punctuation}`;
}
const args = ['Hello', 'Alex', '!'];
console.log(greet(...args)); // "Hello, Alex!"
// Super useful when building wrapper functions
function callWithLogging(fn, ...args) {
console.log(`Calling ${fn.name} with:`, args);
return fn(...args); // Spread back out when calling the wrapped function
}
callWithLogging(greet, 'Hey', 'Sam', '.');
// Calling greet with: ['Hey', 'Sam', '.']
// "Hey, Sam."
Notice the callWithLogging function: it uses rest to collect the extra arguments (...args in the parameter list), then uses spread to pass them along (fn(...args) in the function call). One function, both operators, completely different jobs.
07Two Things Worth Watching Out For
Spread Is a Shallow Copy, Not a Deep One
This is important. When you spread an array or object, you get a one-level-deep copy. If your object contains nested objects or arrays, those nested parts are still shared references:
const original = {
name: 'Sam',
address: { city: 'London' } // Nested object
};
const copy = { ...original };
copy.name = 'Alex'; // Safe: primitive value
copy.address.city = 'Paris'; // Danger: modifies the shared nested object!
console.log(original.name); // 'Sam' ← untouched (primitive)
console.log(original.address.city); // 'Paris' ← changed! (shared reference)
Changing a top-level primitive property on the copy is safe. But the nested address object is the same object in memory for both original and copy. Mutating it from one side affects both.
For nested data, you either spread at each level manually (like we did in the React state example) or use a library like Lodash’s cloneDeep for fully independent deep copies.
Rest Must Always Be Last
Just to drive this home with a clear example:
// This works fine
function logEvent(type, ...data) {
console.log(`[${type}]`, data);
}
logEvent('click', 'button', '#submit', 'primary');
// [click] ['button', '#submit', 'primary']
// This throws a SyntaxError at parse time
function broken(...data, type) { } // ❌ Rest must be last!
// Same rule in destructuring
const [first, ...middle, last] = [1, 2, 3, 4]; // ❌ SyntaxError
const [head, ...tail] = [1, 2, 3, 4]; // ✅ Works fine
JavaScript needs to know where the named parameters end and the “everything else” collection begins. Rest at the end is the only arrangement that makes that unambiguous.
08Quick Reference: Spread and Rest at a Glance
// ── SPREAD OPERATOR ──────────────────────────────
// In function calls: expand array into arguments
Math.max(...[1, 5, 3]); // 5
// Clone an array
const arrClone = [...originalArr];
// Merge arrays
const merged = [...arr1, ...arr2];
// Clone an object
const objClone = { ...originalObj };
// Merge objects (later key wins)
const config = { ...defaults, ...overrides };
// Update one property immutably
const updated = { ...obj, key: newValue };
// ── REST PARAMETER ───────────────────────────────
// Collect all arguments
function sum(...nums) { return nums.reduce((a,b) => a+b, 0); }
// Named params + rest (rest must be last)
function fn(a, b, ...rest) { }
// Rest in array destructuring
const [first, ...others] = [1, 2, 3, 4];
// Rest in object destructuring
const { id, name, ...extra } = user;
Bookmark this if you want. These are the patterns you’ll reach for on a near-daily basis once you’re building real JavaScript applications.
09What’s Coming Up Next
With spread and rest covered, we’ve now wrapped up the entire Variables, Data Types and Operators batch of this JavaScript series. That’s eight articles from var, let, and const all the way through destructuring and now these three little dots.
Next up, we’re moving into Batch 3: Control Flow and Loops. That’s where JavaScript starts making decisions: if statements, ternary expressions, switch, and the various loop types. This is where your code goes from a static list of instructions to something that actually responds and reacts.
If spread and rest clicked for you today, the next batch is going to feel very natural. You’ll start seeing code that does different things based on conditions, and all the data skills you’ve built so far will start serving a real purpose.
Take a few minutes to play with the three interactive demos above if you haven’t already. There’s a difference between reading about [...arr1, ...arr2] and actually typing in values and watching it work. The second one sticks.
See you in the next one.
Spread and Rest Operators
Loading challenge…
Ready to Practice?
Select a difficulty level above to load your challenge.