Home | Send Feedback | Share on Bluesky |

JavaScript spread and rest syntax examples

Published: 19. January 2019  •  javascript

In this blog post, you'll find a collection of examples revolving around the JavaScript spread and rest syntax (...). These are not new examples, and you have probably already seen a few of them on the web. I just wanted to collect them here as a reference.

Introduction

Spread syntax

With spread syntax, JavaScript code can expand any iterable, such as arrays or strings, in place. A use case is passing arrays to methods that expect zero or more arguments instead of arrays.

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

const input = [1, 2, 3];
sum(input); // wrong

When you call the function with sum(input), you get the wrong result. JavaScript assigns the argument input to a, and b and c are undefined. To resolve this, you have to spread the array so that each element is a single argument.

sum(...input);
// equivalent to
sum(input[0], input[1], input[2]);

Common examples that use this pattern are Math.min and Math.max.

Math.min(...input) === 1;
Math.max(...input) === 3;

If you call such a method with fewer arguments than the function expects, the parameters without a value will be set to undefined.

sum(...[1, 2]); // c === undefined

Excessive arguments are ignored.

sum(...[1, 2, 3, 4]); // fourth array element ignored

Another use case for spread syntax is in array literals, where it can copy or merge arrays.

const numbers = [1, 2, 3];
const copyOfNumbers = [...numbers]; // [1, 2, 3]

// merge two arrays
const first = [1, 2, 3];
const second = [4, 5, 6];
const merged = [...first, ...second]; // [1, 2, 3, 4, 5, 6]

Be careful when working with arrays of objects because spread syntax only creates a shallow copy.

const john = {name: 'John'};
const jane = {name: 'Jane'};

const persons = [john, jane];
const copyOfPersons = [...persons]; // copies only the references, not a deep copy

john.name = 'John Doe';

// change is reflected in both arrays
persons[0].name === 'John Doe';
copyOfPersons[0].name === 'John Doe';

Since ES2018, spread syntax also supports objects. You can use this for copying objects or for applying default property values.

const user = {id: 1, name: 'John'};
const copyOfUser = {...user}; // {id: 1, name: 'John'}

Spread syntax only copies own enumerable properties. Be aware that this is just a shallow copy. Nested objects still point to the same object. If you need a deep clone of supported data types, consider structuredClone() instead of spread syntax.

const hobbies = ['reading'];
const person = {id: 2, name: 'Jane', hobbies};
const copyOfPerson = {...person};

hobbies.push('jogging');

// person.hobbies -> ["reading", "jogging"]
// copyOfPerson.hobbies -> ["reading", "jogging"]

const defaultSettings = {timeout: 1000, packetSize: 10};
const userSettings = {packetSize: 20, iterations: 5};

const settings = {...defaultSettings, ...userSettings}; // {timeout: 1000, packetSize: 20, iterations: 5}

Note that the last property always "wins" if the property is part of multiple objects. In this example, the value of packetSize in userSettings overrides the value from defaultSettings.


Source: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax


Rest syntax

It looks the same as spread syntax (...), but it does the opposite. It collects multiple elements and puts them into a single container.

If a function parameter is prefixed with ..., all remaining arguments will be placed into an array. Notice that only the last parameter can be a rest parameter.

function print(a, b, ...others) {
   console.log(a, b, others);
}

print(1, 2, 3, 4, 5, 6);
// a === 1
// b === 2
// others -> [3, 4, 5, 6]

If there are no remaining arguments, the rest parameter is an empty array [].

print(1, 2);
// a === 1
// b === 2
// others -> []

print(1)
// a === 1
// b === undefined
// others -> []

Source: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/rest_parameters

Examples

Converting Maps and Sets to JSON

When you are working with ES2015 Map and Set, you might have noticed that they can't be converted to JSON.

const aMap = new Map().set(1, 'one').set(2, 'two');
const aSet = new Set().add('reading').add('swimming');

const mapJson = JSON.stringify(aMap); // === "{}"
const setJson = JSON.stringify(aSet); // === "{}"

The stringify calls don't fail but return "{}", which is not very useful. One option is to use spread syntax and expand the elements from the collection into an array.

const mapJson = JSON.stringify([...aMap]); // === '[[1,"one"],[2,"two"]]'
const setJson = JSON.stringify([...aSet]); // === '["reading","swimming"]'

From this form, it is also easy to deserialize them back into a Map or Set.

const anotherMap = new Map(JSON.parse(mapJson));
const anotherSet = new Set(JSON.parse(setJson));

If you have an object with nested Maps and Sets, you can use a replacer function to serialize them into JSON.

const obj = {
    hobbies: new Set(['jogging', 'biking']),
    metadata: new Map([['header', 'somedata'], ['auth', 'token']])
};

function replacer(key, value) {
  if (value instanceof Map || value instanceof Set) {
    return [...value];
  }
  return value;
}

const json = JSON.stringify(obj, replacer);
// '{"hobbies":["jogging","biking"],"metadata":[["header","somedata"],["auth","token"]]}'

If your goal is deep cloning or transferring data in memory, structuredClone() is often the more modern choice, but it is not a replacement for JSON serialization.

Source: https://2ality.com/2015/08/es6-map-json.html


Remove elements or properties

With the rest syntax and destructuring, you can "remove" elements from an array. This pattern does not remove elements in place; it creates a copy of an existing array or object and ignores certain elements or properties.

This example copies an array without the first element.

const data = [1, 2, 3];

const [first, ...rest] = data;
// first === 1
// rest -> [2, 3]

Array.shift() does something similar; it removes the first element of an array, but it changes the array, whereas with this pattern, the source array is not changed.

This does not work if you want to create a copy without the last element because the rest element must be the last.

const [...rest, last] = data; // error

This pattern also works with properties in objects (since ES2018).

const user = {
    id: 1,
    username: 'john',
    email: 'jd@email.com',
    password: 'very secret'
};

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

//password === "very secret"
//email === "jd@email.com"
//publicUser -> {id: 1, username: "john"}

The source object user stays unchanged.

Notice that this approach creates variables (password, email, first) in the current scope. If they are not needed, you could rename them to make it more obvious that they should not be used.

// create variables _ and rest
const [_, ...rest] = data;

// create variables _p, _e, and publicUser
const {
    password: _p,
    email: _e,
    ...publicUser
} = user;

Source: https://www.bram.us/2018/01/10/javascript-removing-a-property-from-an-object-immutably-by-destructuring-it/


Conditionally adding elements and properties

If you combine spread syntax with the ternary operator, you have a concise way to add elements to an array based on a condition.

const important = true;
const data = [
  ...(important ? [1, 2, 3] : []),
  4, 5, 6
];

// important === true   -->  data -> [1,2,3,4,5,6]
// important === false  -->  data -> [4,5,6]

This works because spreading an empty array adds no elements.


Since ES2018, you can use the same pattern to conditionally add properties to objects.

const temperature = true;
const sensorData = {
    id: 1,
    sensorName: 'ES9283',
    ...(temperature ? {temp: 23.5} : {}),
    humidity: 67
};

// temperature === true  --> sensorData -> {id: 1, sensorName: 'ES9283', temp: 23.5, humidity: 67}
// temperature === false  --> sensorData -> {id: 1, sensorName: 'ES9283', humidity: 67}

Source: https://2ality.com/2017/04/conditional-literal-entries.html


Remove duplicates from an array

An easy way to remove duplicates from an array is to convert it into a Set and back into an array. The spread syntax expands all elements from the Set into the array literal.

const someData = [1, 1, 1, 2, 3, 4, 4, 4, 4];
const distinctData = [...new Set(someData)]; // [1,2,3,4]

Source: https://www.bram.us/2017/02/07/es2015-es6-easily-remove-duplicates-from-an-array/


Make your objects iterable

Spread syntax uses the iterable protocol to expand elements in array literals and function calls.

[...[1, 2, 3]]
[...'strings']
[...new Map([[1, 'one'], [2, 'two']])]
[...new Set(['reading', 'swimming'])]

Plain objects are not iterable by default, so [...{a: 'one', b: 'two'}] throws a TypeError, even though object spread works inside an object literal such as {...obj}.

If you create your own class and then try to use spread syntax, an error occurs because your object does not implement the iterable protocol by default.

class Hobbies {
  constructor() {
    this.hobby1 = 'reading';
    this.hobby2 = 'swimming';
  }
}

const hobbies = new Hobbies();
const expanded = [...hobbies]; // TypeError: object is not iterable

To make this class iterable, we have to implement the iterator method, which has to return an object with a next() function. This can be simplified by implementing a generator function that returns a Generator object, and this object conforms to the iterable protocol because it implements the next method.

class Hobbies {
  constructor() {
    this.hobby1 = 'reading';
    this.hobby2 = 'swimming';
  }

  *[Symbol.iterator]() {
      yield this.hobby1;
      yield this.hobby2;
  }
}

const hobbies = new Hobbies();
const expanded = [...hobbies]; // ['reading', 'swimming']