Javascript Variables, Data Types & Operators Spread and Rest Operators

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.






Result


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.

Object 1 (defaults)
theme“dark”
fontSize14
notifytrue
lang“en”
Object 2 (user prefs)
fontSize“20”
theme“light”
sidebarfalse
Spread order:

Merged Result

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.

function describe(name, role, …skills) { … }


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

Choose a difficulty to load your challenge

Loading challenge…

Ready to Practice?

Select a difficulty level above to load your challenge.

Easy

1
Live Preview
Great Work!