JavaScript three dots (...) in depth

The three dots (...) in JavaScript can be tricky to understand because the 3 dots are used to represent 2 complementary concepts (rest and spread).

Rest packs data and spread unpacks data.

I'll explain why use spread and rest and then show a bunch of examples.

Why use the spread operator?

Let's say you want to create an array with the strings 'a' and 'b':

const arr = ['a', 'b'];

Now let's create an array with 'a', 'b' and the contents of the array ['c', 'd']:

const arr1 = ['c', 'd'];
const arr2 = ['a', 'b', arr1];
console.log(arr2); // ['a', 'b', ['c', 'd']]

Wait! I don't want an array inside another array, I want the contents of arr1 inside arr2. This should work:

const arr1 = ['c', 'd'];
const arr2 = ['a', 'b'].concat(arr1);
console.log(arr2); // ['a', 'b', 'c', 'd']

Now I want to have 'a', 'b', followed by the contents of the array ['c', 'd'] followed by 'e', followed by the contents of the array ['f', 'g']:

const arr = [];
const arr1 = ['c', 'd'];
const arr2 = ['f', 'g'];

arr.push('a');
arr.push('b');
arr.concat(arr1);
arr.push('e');
arr.concat(arr2);
console.log(arr); // ['a', 'b', 'c', 'd', 'e', 'f', 'g']

That's a lot of lines just because I want to mix string literals with array literals.

What if there was a way to spread the contents of an array inside another array? It's the spread operator!

const arr1 = ['c', 'd'];

const arr = ['a', 'b', ...arr1];
console.log(arr); // ['a', 'b', 'c', 'd']

Nice! What about the more complex example?

const arr = [];
const arr1 = ['c', 'd'];
const arr2 = ['f', 'g'];

arr = ['a', 'b', ...arr1, 'e', ...arr2];
console.log(arr); // ['a', 'b', 'c', 'd', 'e', 'f', 'g']

You can read the assignment of arr as:
Create an array with 'a'; 'b'; the contents of arr1; 'e'; the contents of arr2.

Why use rest parameters?

Imagine you want to get the first and second element of an array and put the rest of the array on a variable:

const arr = [10, 20, 30, 40];
const first = arr[0];
const second = arr[1];
const rest = arr.slice(2);

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

Looks easy enough although you probably checked how slice works.

How can we do this with rest elements?

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

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

The array on the left is matching the array on the right. So first matches with 10, second matches with 20 and rest matches with [30, 40].

Like so:

Rest parameter diagram

Why three dots for rest and spread?

By now you might be thinking that spread unpacks the elements inside an array and rest packs remaining elements in an array. Why use the same three dots?

Let's use rest elements and the spread operator in a single example:

const [first, second, ...rest] = ['a', 'b', 'c', 'd'];
const assembled = [first, second, ...rest];

console.log(assembled); // ['a', 'b', 'c', 'd'];

You noticed what just happened? The code with the rest elements [first, second, ...rest] can be copy pasted as a spread operator and the result is the original array literal!

Spread and rest fit like 2 peas in a pod!

Now let's take a look how to use rest and spread in array literals and object literals.

Arrays (spread and rest)

The main rule for the rest elements when destructuring an array is:

You can only have the rest parameter at the end of the array.

You'll see an example of this in the code below.

There are not a lot of restrictions for the spread operator. The examples below provide a good overview of how it works.

const l1 = [1, 2];
const l2 = [3, 4];

//// SPREAD
console.logs([0, ...l1]); // [0, 1, 2]
console.logs([...l1, 3]); // [1, 2, 3]
console.logs([0, ...l1, 3]); // [0, 1, 2, 3]
console.log([...l1, ...l2]); // [1, 2, 3, 4]

const l3 = [...l1];
console.log(l3); // [1, 2]
l1 === l3; // false, it created a new array AND didn't change the original array

//// REST
let d1 = [1, 2, 3];

const [a, ...alpha] = d1;
console.log(a); // 1
console.log(alpha); // [2, 3]

// You don't need to use a variable. You can use an array literal directly.
const [b, ...beta] = [10, 20, 30];
console.log(b); // 10
console.log(beta); // [20, 30]

// SyntaxError: Rest parameter must be last formal parameter
const [...charlie, c] = [10, 20, 30];

//// FUNCTION SPREADING
function sum(x, y) {
  return x + y;
}

console.log(sum(...l1)); // 2
console.log(sum(l1)); // "1,2undefined"

//// FUNCTION REST
function sayMsg(msg, ...arr) {
  console.log(msg);
  arr.forEach((i) => console.log(i));
}

console.log(sayMsg('Hi', 1, 2, 3)); // "Hi" 1 2 3

Math.max([3, 1, 2]); // NaN, max doesn't accept an array
Math.max(...[3, 1, 2]); // 3

// SyntaxError: Rest parameter must be last formal parameter
function f1(...arr, val){} // Rest parameter must be the last
function f2(...arr1, ...arr2){} // There can only be one rest parameter

Objects (spread and rest)

Rest properties and spread properties for object literals are similar to the rest and spread elements for arrays.

But instead of array elements you have property keys of an object.

Check it out:

const { a, b, ...rest } = { a: 1, b: 2, c: 3, d: 4 };
const assembled = { a, b, ...rest };

console.log(a); // 1
console.log(b); // 2
console.log(rest); // { c: 3, d: 4 }
console.log(assembled); // { a: 1, b: 2, c: 3, d: 4 }

The example above shows how the rest and spread work so well together to disassemble and assemble an object. The { a, b, ...rest } snippet is used with rest and also spread.

Let's see some more examples:

let o1 = { a: 1, b: 2 };
let o2 = { c: 3, d: 4 };
let o3;

//// SPREAD

o3 = { k: 'v', ...o1 };
console.log(o3); // { k: "v", a:1, b: 2 }

o3 = { ...o1, k: 'v' };
console.log(o3); // { a:1, b: 2, k: "v" }

o3 = { ...o1, ...o2 };
console.log(o3); // {a: 1, b: 2, c: 3, d: 4}

// If an existing property appears at the end, it will overwrite the existing
// property but keeps it in the same position.
o3 = { ...o1, a: 'aa' };
console.log(o3); // { a: 'aa', b: 2 }

o3 = { a: 'aa', ...o1 };
console.log(o3); // { a: 1, b: 2 }

// You can spread an object literal directly. No need to have a variable.
const o4 = { a: 1, ...{ b: 2, c: 3 } };
console.log(o4); // { a: 1, b: 2, c: 3 }

//// DESTRUCTURING

const { k1, ...rest } = { k1: 'v1', k2: 'v2', k3: 'v3' };
console.log(k1); // 'v1'
console.log(rest); //  { k2: 'v2', k3: 'v3' }

// Error: Rest element must be last element
const {  ...rest, k1 } = { k1: 'v1', k2: 'v2', k3: 'v3' };

//// FUNCTION SPREADING

function log({ msg1, msg2, msg3 }) {
  console.log(msg1, msg2, msg3);
}

const args = { msg2: 'hi', msg3: 'there' };
log({ msg1: 'Oh', ...args }); // Oh hi there

//// FUNCTION REST
function f({ msg, ...rest }) {
  console.log(msg, '--', Object.keys(rest));
}

f({ msg: 'hi', a: 1, b: 2, c: 3 }); // hi -- [ 'a', 'b', 'c']

Bonus properties

When you spread, it creates a shallow copy. So the objects inside are not a copy.

const person = { name: 'John', location: { country: 'Luxembourg' } };
const copy = { ...person };

// Changes only copy
copy.name = 'Jane';
console.log(person.name); // "John"
console.log(copy.name); // "Jane"

// Changes on both objects
copy.location.country = 'North Korea';
console.log(person.location.country); // "North Korea"

You can destructure from an array to an object. This is assigning the results to new variables that we didn't see here. Check MDN destructuring assignment for more.

// Assign element 0 to variable bla and element 1 to variable ble
const { 0: bla, 1: ble } = ['a', 'b'];
console.log(bla, ble); // "a" "b"

Rest elements must be the last element, but if nested you can have multiple. It's hard to read though.

const p = {
  name: 'Jane',
  age: 18,
  location: { country: 'Metro Kingdom', city: 'New Donk City' },
};

const {
  location: { country, ...rest1 },
  ...rest2
} = p;

console.log(rest1); // {city: 'New Donk City'}
console.log(rest2); // { name: 'Jane', age: 18 }

You can get rid of properties you know creating a simple function:

// Get rid of name and age:
const sanitize = ({ name, age, ...rest }) => rest;

const o = { name: 'Johnny Boy', age: 30, job: 'snoozer' };
console.log(sanitize(o)); // { job: 'snoozer' }

Trivia

The following information is not important but I find it interesting.

  • The spread/rest for array literals was introduced with ES2015. And in ES2016 it was introduced for object literals.
  • A function that accepts a varying number of arguments is called a variadic function.
  • The spread/rest for object literals was proposed by Sebastian Markbåge. He is a member of the React core team.

Questions

What's the console output?

const arr1 = [30];
console.log([4, ...arr1]);

What's the console output?

function sum(x, y) {
  return x + y;
}

console.log(sum([1, 1]));

What's the console output?

// Math.max(3, 1, 4); => returns the max (4), notice parameter is not an array.
const arr = [6, 2, 1];

console.log(Math.max(arr));

What's the console output?

// Math.max(3, 1, 4); => returns the max (4), notice parameter is not an array.
const arr = [6, 2, 1];

console.log(Math.max(...arr));

What's the console output?

function f(m, ...rest) {
  return rest.map((el) => m * el);
}

const res = f(2, 5, 5);
console.log(res);

What's the console output?

function f(...rest, m) {
  return rest.map((el) => m * el);
}

const res = f(5, 5, 2);
console.log(res);

What's the console output?

function f(m, ...rest1, ...rest2) {
  return rest2.map((el) => m * el);
}

const res = f(2, 5, 5, 2, 2);
console.log(res);

What's the console output?

const { a, b, ...c } = { a: 1, b: 2, c: 3, d: 4 };
const o = { a, b, ...c };
console.log(o);

What's the console output?

const ab = { a: 1, b: 2 };
console.log({ ...ab, k: 'v' });

What's the console output?

const ab = { a: 1, b: 2 };
console.log({ x: 'x', ...ab, y: 'y', ...ab });

What's the console output?

const ab = { a: 1, b: 2 };
const c = { c: 3 };
console.log({ x: 1, ...ab, y: 2, ...c });

What's the console output?

const arr1 = [20];
console.log([...arr1, 7]);

What's the console output?

const a = { a: 1 };
const b = { b: 2 };
console.log({ ...a, ...b });

What's the console output?

console.log({ k: 'v', ...{ a: 1 } });

What's the console output?

const kk = { k1: 1, k2: 2 };
const { k1, ...b } = kk;
console.log(k1, b);

What's the console output?

const { a, ...b } = { k1: 1, k2: 2 };
console.log(typeof a);

What's the console output?

const kk = { k1: 1, k2: 2 };
const {...rest, k2} = kk;
console.log(rest)

What's the console output?

function f({ a, b, c }) {
  console.log(b);
}

const args = { a: 1 };
f({ args, b: 2 });

What's the console output?

function f({ a, ...rest }) {
  console.log(rest);
}

f({ a: 1, b: 2 });

What's the console output?

const o1 = { a: 1, b: { x: 1 } };
const o2 = { ...o1 };

o2.b.x = 'banana';

console.log(o1.b.x, o2.b.x);

What's the console output?

const o1 = { a: 1 };
const o2 = { ...o1 };

o2.a = 'banana';

console.log(o1.a, o2.a);

What's the console output?

const { 2: a } = ['a', 'b', 'c'];
console.log(a);

What's the console output?

const arr = ['apple'];
console.log(['tomato', ...arr, 'peach']);

What's the console output?

const p = {
  name: 'Jane',
  age: 18,
  location: { country: 'Luxembourg', city: 'Luxembourg' },
};

const {
  location: { country, ...rest1 },
  age,
  ...rest2
} = p;
console.log(rest1.city, rest2.name);

What's the console output?

const sanitize = ({ id, ...rest }) => rest;

const clean = sanitize({ id: 1, name: 'Bruce' });
console.log(clean);

What's the console output?

const arr1 = ['apple', 'pie'];
const arr2 = ['spinach', 'teeth'];
console.log([...arr1, ...arr2]);

What's the console output?

const arr1 = ['apple', 'pie'];
const arr2 = arr1;
const arr3 = [...arr1];

console.log(arr1 === arr2, arr1 === arr3);

What's the console output?

console.log([1, ...['a']]);

What's the console output?

const [a, ...r] = [1, 2, 3];
console.log(a, r);

What's the console output?

const [...r, a] = [1, 2, 3];
console.log(a, r);

What's the console output?

function sum(x, y) {
  return x + y;
}

console.log(sum(...[2, 2]));