Posts

JavaScript Spread Syntax Explained

The JavaScript spread ... syntax is amazing. What it does is let you take the individual items from an array, or the individual properties from an object, and spread them out into another place. That “another …

March 3, 2022 9 min read 1899 words

The JavaScript spread ... syntax is amazing.

What it does is let you take the individual items from an array, or the individual properties from an object, and spread them out into another place.

That “another place” might be:

  • a new array
  • a new object
  • the arguments being passed into a function

When I first learned to use spread syntax I was a little confused about what it was actually doing. The three little dots looked simple enough, but JavaScript has a long history of making simple-looking things weird.

So let’s walk through it slowly.

Let’s start with an object named hand.

const hand = {
  thumb: 1,
  index: 2,
  middle: 3,
  ring: 4,
  pinkie: 5
};

If we spread the hand object, each property can be treated as its own separate value.

const handCopy = { ...hand };

console.log(handCopy);
// { thumb: 1, index: 2, middle: 3, ring: 4, pinkie: 5 }

In my head, spread syntax is kind of like taking a hand apart and treating each finger as its own separate value.

JavaScript spread syntax shown as detachable fingers from a hand

What Spread Syntax Looks Like

Spread syntax uses three dots before a value:

...thing

That value needs to be something JavaScript can spread out.

For arrays, JavaScript spreads out the items.

For objects, JavaScript spreads out the properties.

That is the big idea.

const numbers = [1, 2, 3];

console.log(...numbers);
// 1 2 3

The array itself was not logged as one value. The items inside the array were spread out as individual values.

Tiny syntax. Big difference.

Spreading Arrays

Let’s start with an array.

const fingers = ['thumb', 'index', 'middle'];

If we want to create a new array that includes everything in fingers, plus a few more values, we can use spread syntax.

const hand = [...fingers, 'ring', 'pinkie'];

console.log(hand);
// ['thumb', 'index', 'middle', 'ring', 'pinkie']

This line:

const hand = [...fingers, 'ring', 'pinkie'];

is basically saying:

“Create a new array, put every item from fingers into it, then add ring and pinkie after that.”

Without spread syntax, you might have done something like this:

const hand = fingers.concat(['ring', 'pinkie']);

That works too, but spread syntax is usually easier to read once you know what the dots are doing.

Copying Arrays With Spread

Spread syntax is also commonly used to copy an array.

const original = ['thumb', 'index', 'middle'];
const copy = [...original];

console.log(copy);
// ['thumb', 'index', 'middle']

That gives us a new array with the same values.

Important detail: it is a new array.

console.log(original === copy);
// false

The values inside may be the same, but the array itself is a different array.

This is useful when you want to make changes without mutating the original array.

const original = ['thumb', 'index', 'middle'];
const updated = [...original, 'ring'];

console.log(original);
// ['thumb', 'index', 'middle']

console.log(updated);
// ['thumb', 'index', 'middle', 'ring']

The original array stayed untouched.

That is a big deal in many JavaScript codebases, especially when working with frameworks like React where immutable updates help keep state changes predictable.

Combining Arrays

Spread syntax also makes it simple to combine arrays.

const leftHand = ['left thumb', 'left index'];
const rightHand = ['right thumb', 'right index'];

const hands = [...leftHand, ...rightHand];

console.log(hands);
// ['left thumb', 'left index', 'right thumb', 'right index']

You can also place values before, between, or after the arrays.

const hands = ['start', ...leftHand, 'middle', ...rightHand, 'end'];

console.log(hands);
// ['start', 'left thumb', 'left index', 'middle', 'right thumb', 'right index', 'end']

The spread parts are unpacked in the exact position where you put them.

Spreading Strings

Strings can also be spread because strings are iterable.

Fancy word. Simple behavior.

const name = 'Rob';
const letters = [...name];

console.log(letters);
// ['R', 'o', 'b']

JavaScript walked through the string one character at a time and put each character into the new array.

Pretty neat.

Also pretty easy to abuse, so do not get too excited.

Spreading Arrays Into Function Calls

Spread syntax can also be used when calling a function.

Let’s say we have a function that expects three separate arguments.

function add(a, b, c) {
  return a + b + c;
}

And we have an array of numbers.

const numbers = [1, 2, 3];

Without spread syntax, we might call the function like this:

add(numbers[0], numbers[1], numbers[2]);

That works, but it is clunky.

With spread syntax we can do this:

const total = add(...numbers);

console.log(total);
// 6

The array values were spread out and passed into the function as separate arguments.

So this:

add(...numbers);

becomes this:

add(1, 2, 3);

That is one of the places where spread syntax really earns its keep.

Spreading Objects

Now let’s get deeper into objects.

Here is that hand object again.

const hand = {
  thumb: 1,
  index: 2,
  middle: 3,
  ring: 4,
  pinkie: 5
};

If we want to copy that object, we can use spread syntax inside a new object.

const handCopy = { ...hand };

console.log(handCopy);
// { thumb: 1, index: 2, middle: 3, ring: 4, pinkie: 5 }

Again, this creates a new object.

console.log(hand === handCopy);
// false

The properties were copied into a new object, but hand and handCopy are not the same object.

Adding Object Properties With Spread

You can also use spread syntax to copy an object and add new properties at the same time.

const hand = {
  thumb: 1,
  index: 2,
  middle: 3
};

const fullHand = {
  ...hand,
  ring: 4,
  pinkie: 5
};

console.log(fullHand);
// { thumb: 1, index: 2, middle: 3, ring: 4, pinkie: 5 }

This is one of the most common ways you will see object spread used.

It is especially useful when you want to create an updated version of an object without changing the original.

const user = {
  name: 'Sam',
  role: 'reader'
};

const updatedUser = {
  ...user,
  role: 'admin'
};

console.log(user);
// { name: 'Sam', role: 'reader' }

console.log(updatedUser);
// { name: 'Sam', role: 'admin' }

The original user object did not change. We created a new object and changed the role value in the new object.

Property Order Matters

When spreading objects, property order matters.

If the same property exists more than once, the last value wins.

const user = {
  name: 'Sam',
  role: 'reader'
};

const updatedUser = {
  ...user,
  role: 'admin'
};

console.log(updatedUser);
// { name: 'Sam', role: 'admin' }

The role property from user was copied first.

Then role: 'admin' came after it and overwrote the previous value.

But if we reverse the order:

const updatedUser = {
  role: 'admin',
  ...user
};

console.log(updatedUser);
// { role: 'reader', name: 'Sam' }

Now the role from user wins because it came last.

This is a small detail that can cause big confusion.

Refactor Rob: When object spread is used for updates, put the original object first and the changed properties after it. Future-you will have a much easier time reading what changed.

Spread Makes Shallow Copies

Here is the part that trips people up.

Spread syntax makes a shallow copy.

That means it copies the first level of values.

If one of those values is another object or array, the nested object or array is still shared.

JavaScript shallow copy shown as two objects sharing the same nested settings object

const user = {
  name: 'Sam',
  settings: {
    theme: 'dark'
  }
};

const copy = { ...user };

copy.settings.theme = 'light';

console.log(user.settings.theme);
// light

Wait. What?

We changed copy.settings.theme, but user.settings.theme changed too.

That happened because settings is an object. Spread copied the reference to that nested object, not a brand new nested object.

So user.settings and copy.settings still point to the same nested object.

To copy the nested object too, you need to spread that level as well.

const user = {
  name: 'Sam',
  settings: {
    theme: 'dark'
  }
};

const copy = {
  ...user,
  settings: {
    ...user.settings,
    theme: 'light'
  }
};

console.log(user.settings.theme);
// dark

console.log(copy.settings.theme);
// light

That is better.

Still, this does not mean you should manually spread ten levels deep. If your object is that nested, there may be a better data shape hiding in there somewhere.

Spread Syntax vs Rest Syntax

Here is where the three dots get extra annoying.

The same ... syntax can mean “spread” or “rest” depending on where it appears.

Spread expands values out.

const numbers = [1, 2, 3];

console.log(...numbers);
// 1 2 3

Rest gathers values together.

function logNumbers(...numbers) {
  console.log(numbers);
}

logNumbers(1, 2, 3);
// [1, 2, 3]

Same three dots. Opposite direction.

Spread:

add(...numbers);

The array is opened up and sent into the function as individual arguments.

Rest:

function add(...numbers) {
  // numbers is an array
}

The individual arguments are gathered into one array.

So if the dots are being used where values are being provided, think spread.

If the dots are being used where values are being received, think rest.

That mental model is not perfect, but it gets you pretty far.

Common Spread Syntax Examples

Here are a few examples you will probably see in real code.

Add an Item to an Array

const items = ['one', 'two'];
const updatedItems = [...items, 'three'];

console.log(updatedItems);
// ['one', 'two', 'three']

Add an Item to the Beginning of an Array

const items = ['two', 'three'];
const updatedItems = ['one', ...items];

console.log(updatedItems);
// ['one', 'two', 'three']

Merge Objects

const defaults = {
  theme: 'light',
  showSidebar: true
};

const preferences = {
  theme: 'dark'
};

const settings = {
  ...defaults,
  ...preferences
};

console.log(settings);
// { theme: 'dark', showSidebar: true }

Update One Property

const todo = {
  id: 1,
  text: 'Learn spread syntax',
  complete: false
};

const completedTodo = {
  ...todo,
  complete: true
};

console.log(completedTodo);
// { id: 1, text: 'Learn spread syntax', complete: true }

Remove a Property Without Mutating the Original

This one actually uses rest syntax during destructuring, but you will see it near spread-heavy code all the time.

const user = {
  id: 1,
  name: 'Sam',
  password: 'super-secret'
};

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

console.log(publicUser);
// { id: 1, name: 'Sam' }

The password property was pulled out, and the rest of the properties were gathered into publicUser.

See? Same dots. Different job.

JavaScript, everybody.

When Not to Use Spread

Spread syntax is useful, but it is not magic seasoning you sprinkle on everything.

Avoid spread when it makes the code harder to understand.

const result = [...[...[...[1, 2, 3]]]];

Please do not.

Also be careful with very large arrays.

someFunction(...veryLargeArray);

Spreading a huge array into a function call can run into engine limits because each item becomes a separate argument.

For normal day-to-day arrays this is usually fine. For massive arrays, find another approach.

And remember the shallow copy rule. Spread is not a deep clone.

If you need a true deep clone, spread syntax is not enough by itself.

Quick Recap

Spread syntax uses ... to expand values out.

With arrays:

const numbers = [1, 2, 3];
const moreNumbers = [...numbers, 4, 5];

With objects:

const user = { name: 'Sam' };
const admin = { ...user, role: 'admin' };

With function calls:

const numbers = [1, 2, 3];
add(...numbers);

The main things to remember:

  • spread arrays into individual items
  • spread objects into individual properties
  • spread function arguments out from an array
  • object and array spread create shallow copies
  • the last object property wins when properties have the same name
  • ... can also mean rest syntax, depending on where it is used

Once you get used to it, spread syntax becomes one of those JavaScript features you reach for constantly.

Just keep an eye on which direction the dots are moving.