JavaScript and TypeScript course Β· Module 2: Functions, Arrays and Objects

Spread and Rest Operators

7 min read
In this lesson6

In Jurassic Park we constantly combine data from different monitoring systems, copy sets of parameters and handle functions that receive two samples one day and ten the next. The spread (...) and rest (...) operators use the same notation, three dots, but play two opposite roles. Spread "unpacks" elements, while rest "packs" them into a single structure. "It is the park's transport system," says Dr. Rex. "Spread unloads sample containers onto separate workstations, and rest packs many samples into one container."

Spread Operator for Arrays

Spread "spreads" array elements into a place where individual values are expected. This is how you merge the list of carnivores with the list of herbivores:

1const carnivores = ['T-Rex', 'Velociraptor', 'Spinosaurus'];
2const herbivores = ['Triceratops', 'Brachiosaurus', 'Stegosaurus'];
3
4// Merging arrays
5const allDinos = [...carnivores, ...herbivores];
6console.log(allDinos);
7// ['T-Rex', 'Velociraptor', 'Spinosaurus', 'Triceratops', 'Brachiosaurus', 'Stegosaurus']
8
9// Adding elements while merging
10const withNew = ['Pteranodon', ...carnivores, 'Dilophosaurus'];
11console.log(withNew);
12// ['Pteranodon', 'T-Rex', 'Velociraptor', 'Spinosaurus', 'Dilophosaurus']

The result is a single flat array, not an array of arrays, because spread inserts the elements rather than whole arrays. Writing [carnivores, herbivores] without the dots would give two nested containers. Both source arrays stay unchanged.

Copying an Array (Shallow Copy)

Spread creates a shallow copy of an array, a new array with the same elements. The syntax is const copy = [...original]:

1const originalZones = ['A', 'B', 'C'];
2const zonesCopy = [...originalZones];
3
4zonesCopy.push('D');
5console.log(originalZones); // ['A', 'B', 'C'] - original unchanged!
6console.log(zonesCopy);     // ['A', 'B', 'C', 'D']

Warning - shallow copy! If the array contains objects, only the references to them are copied, not the objects themselves:

1const dinos = [{ name: 'Rex', health: 100 }];
2const dinosCopy = [...dinos];
3
4dinosCopy[0].health = 50;
5console.log(dinos[0].health); // 50 - the original changed too!
6// Because both elements point to the same object in memory

Only the outer container is new, while the nested objects still share references with the original. When you need an independent, deep copy, I recommend the built-in structuredClone() function.

Spread with Functions

Spread also lets you "unpack" an array into the arguments of a function that expects separate values, such as Math.max:

1const temperatures = [36.5, 37.2, 38.1, 35.8, 39.0];
2
3// Instead of Math.max(36.5, 37.2, 38.1, 35.8, 39.0)
4const maxTemp = Math.max(...temperatures);
5const minTemp = Math.min(...temperatures);
6console.log('Max:', maxTemp); // 39
7console.log('Min:', minTemp); // 35.8

Without the dots, Math.max(temperatures) would receive a single array and return NaN. The console prints 39, not 39.0, because numbers do not store trailing zeros. Spread works on any iterable value, so [...'DNA'] gives ['D', 'N', 'A'].

Spread Operator for Objects

Spread also works with objects and "unpacks" key-value pairs into a new object:

1const baseDino = { species: 'Unknown', health: 100, zone: 'X' };
2
3// Copying an object
4const dinoCopy = { ...baseDino };
5console.log(dinoCopy); // { species: 'Unknown', health: 100, zone: 'X' }
6
7// Overriding properties (the last one wins!)
8const rex = { ...baseDino, species: 'T-Rex', zone: 'A' };
9console.log(rex); // { species: 'T-Rex', health: 100, zone: 'A' }
10
11// Merging objects
12const position = { lat: 12.34, lon: 56.78 };
13const status = { health: 85, alert: false };
14const fullReport = { ...position, ...status, timestamp: Date.now() };
15console.log(fullReport);
16// { lat: 12.34, lon: 56.78, health: 85, alert: false, timestamp: ... }

Spread copies an object's own enumerable properties, including those whose key is a Symbol. baseDino remains a template from which you can create more specimens.

Order Matters

When merging objects, later properties override earlier ones. That is why you always unpack the defaults first:

1const defaults = { voltage: 10000, locked: true, zone: 'A' };
2const userSettings = { voltage: 15000, zone: 'B' };
3
4// Defaults first, then override
5const config = { ...defaults, ...userSettings };
6console.log(config);
7// { voltage: 15000, locked: true, zone: 'B' }
8
9// Reversed order - the defaults would override the settings!
10const wrong = { ...userSettings, ...defaults };
11console.log(wrong);
12// { voltage: 10000, zone: 'A', locked: true } - wrong!

A key's position is set by its first appearance, and its value by the last one. That is why the keys in wrong come in a different order and the default values won. In the same way, { ...{ a: 1, b: 2 }, ...{ b: 3, c: 4 } } gives { a: 1, b: 3, c: 4 }.

Rest Operator in Functions

Rest (...) in function parameters collects all remaining arguments into an array. An observer can report any number of species:

1// Rest parameter - collects a variable number of arguments
2function logSighting(observer, ...dinosaurs) {
3  console.log(`${observer} observed: ${dinosaurs.join(', ')}`);
4  console.log(`Species count: ${dinosaurs.length}`);
5}
6
7logSighting('Dr. Rex', 'T-Rex', 'Velociraptor', 'Triceratops');
8// "Dr. Rex observed: T-Rex, Velociraptor, Triceratops"
9// "Species count: 3"
10
11logSighting('Dr. Rex', 'Brachio');
12// "Dr. Rex observed: Brachio"
13// "Species count: 1"

The first argument goes to observer, and everything else goes to a real array dinosaurs, so join and length work on it. The older arguments object is not an array, which is why I recommend rest in new code.

Rest Must Be the Last Parameter

Rest collects everything that is left, so it has to come at the end of the parameter list. Otherwise JavaScript would not know where to stop collecting:

1// Correct - rest at the end
2function feedDinos(keeper, time, ...dinoNames) {
3  dinoNames.forEach(name => {
4    console.log(`${keeper} feeds ${name} at ${time}`);
5  });
6}
7
8feedDinos('Alan', '08:00', 'Rex', 'Blue', 'Delta');
9
10// Error! Rest cannot come before other parameters
11// function wrong(...names, time) {} // SyntaxError!

A function can have only one rest parameter, and it is always the last one. Any other position ends with a SyntaxError before the code even runs.

Rest in Destructuring

Rest also works in array and object destructuring, which you know from the previous lesson:

1// Rest in array destructuring
2const rankings = ['T-Rex', 'Velociraptor', 'Spinosaurus', 'Giganotosaurus'];
3const [champion, runnerUp, ...others] = rankings;
4console.log(champion);  // "T-Rex"
5console.log(runnerUp);  // "Velociraptor"
6console.log(others);    // ["Spinosaurus", "Giganotosaurus"]
7
8// Rest in object destructuring
9const sensor = { id: 'S-001', value: 42, unit: 'C', zone: 'A', active: true };
10const { id: sensorId, value: sensorValue, ...metadata } = sensor;
11console.log(sensorId);    // "S-001"
12console.log(sensorValue); // 42
13console.log(metadata);    // { unit: 'C', zone: 'A', active: true }

In both cases rest creates a new array or a new object with everything that was not named.

Practical Patterns

Removing Properties from an Object

Combining destructuring with rest lets you leave out fields without modifying the object, for example to hide internal data before sending a report:

1const dino = { name: 'Rex', _secret: 'code', health: 100, _internal: true };
2
3// Pull out what you want to remove, the rest stays
4const { _secret, _internal, ...publicData } = dino;
5console.log(publicData); // { name: 'Rex', health: 100 }

The dino object still has _secret. The extracted fields became separate variables, and publicData is a new object without them.

Creating a Copy with Modifications

Spread is also great for updating data. Instead of changing the original, you create a new version of it with overridden fields:

1function updateDino(dino, updates) {
2  return { ...dino, ...updates, lastModified: Date.now() };
3}
4
5const rex = { name: 'Rex', health: 100, zone: 'A' };
6const updatedRex = updateDino(rex, { health: 85, zone: 'B' });
7console.log(updatedRex);
8// { name: 'Rex', health: 85, zone: 'B', lastModified: ... }
9console.log(rex.health); // 100 - original unchanged!

The old record stays untouched, so it is easy to compare the state before and after the change. State updates in React and Redux are built on this pattern.

Practice

The sandbox contains array merging, copies, Math.max with spread, the order of merging objects and rest in functions. Swap the order of the spreads and check which value wins.

Code for this lesson: index.js
1// === SPREAD AND REST OPERATORS ===
2
3// 1. Spread - combining arrays
4const carnivores = ['T-Rex', 'Velociraptor', 'Spinosaurus'];
5const herbivores = ['Triceratops', 'Brachiosaurus'];
6const allDinos = [...carnivores, ...herbivores];
7console.log('=== Spread arrays ===');
8console.log('All:', allDinos);
9
10// 2. Copying array (shallow copy)
11const original = ['A', 'B', 'C'];
12const copy = [...original];
13copy.push('D');
14console.log('\n=== Copying ===');
15console.log('Original:', original);
16console.log('Copy:', copy);
17
18// 3. Spread with functions
19const temps = [36.5, 37.2, 38.1, 35.8, 39.0];
20console.log('\n=== Spread with functions ===');
21console.log('Max temp:', Math.max(...temps));
22console.log('Min temp:', Math.min(...temps));
23
24// 4. Spread for objects
25const baseDino = { species: 'Unknown', health: 100 };
26const rex = { ...baseDino, species: 'T-Rex', zone: 'A' };
27console.log('\n=== Spread objects ===');
28console.log('Rex:', JSON.stringify(rex));
29
30// Combining objects
31const position = { lat: 12.34, lon: 56.78 };
32const status = { health: 85, alert: false };
33const fullReport = { ...position, ...status };
34console.log('Report:', JSON.stringify(fullReport));
35
36// 5. Order matters
37const defaults = { voltage: 10000, locked: true };
38const settings = { voltage: 15000 };
39console.log('\n=== Order ===');
40console.log('Correct:', JSON.stringify({ ...defaults, ...settings }));
41console.log('Bad:', JSON.stringify({ ...settings, ...defaults }));
42
43// 6. Rest in functions
44function logSighting(observer, ...dinosaurs) {
45  console.log(observer + ' sees: ' + dinosaurs.join(', '));
46  console.log('Count: ' + dinosaurs.length);
47}
48console.log('\n=== Rest in functions ===');
49logSighting('Dr. Rex', 'T-Rex', 'Velociraptor', 'Triceratops');
50
51// 7. Rest in destructuring
52const [champion, runnerUp, ...others] = ['T-Rex', 'Raptor', 'Spino', 'Giga'];
53console.log('\n=== Rest in destructuring ===');
54console.log('Champion:', champion);
55console.log('Rest:', others);
56
57const { name, ...dinoStats } = { name: 'Rex', weight: 8000, zone: 'A' };
58console.log('Name:', name);
59console.log('Stats:', JSON.stringify(dinoStats));

Remember: the same three dots on the right-hand side unload a container, while in parameters and on the left of an assignment they pack samples into a new one.

Spotted a mistake in this lesson?

Check yourself

Answer the questions from this lesson. Pick an answer to see right away whether it is correct.

  1. 1. What will [...['A', 'B'], ...['C', 'D']] return?

  2. 2. What will { ...{ a: 1, b: 2 }, ...{ b: 3, c: 4 } } return?

  3. 3. Where must the rest parameter (...args) be in the function parameter list?

  4. 4. Why does spread create a 'shallow copy' of objects?

Hands-on tasks in the game

  • Code editor

    You have the arrays const carnivores = ['T-Rex', 'Velociraptor'] and const herbivores = ['Triceratops'] and the object const defaults = { zone: 'A', locked: true }. Using the spread operator, create: allDinos (both arrays merged), carnivoresCopy (a new copy of carnivores) and config (defaults overridden with zone: 'B'). Also write a function countSightings(observer, ...dinos) that returns the number of dinosaurs passed.

  • Horizontal ordering

    Arrange the elements of creating an array copy using spread:

  • Code editor

    You have an object const dino = { name: 'Rex', health: 100, _secret: 'code', _internal: true }. Using destructuring with the rest operator, create a new object publicData without the _secret and _internal fields (dino must stay unchanged). Write a function updateDino(dino, updates) that returns a new object: a copy of dino overridden with the fields from updates, without changing the object passed in.

  • Code editor

    Create two DNA sample identifiers: const sampleId1 = Symbol('DNA-001') and const sampleId2 = Symbol('DNA-001'). Display the result of sampleId1 === sampleId2 in the console. Then create an object specimen with the property name: 'T-Rex' and the sample data (e.g. { purity: 98.5 }) stored under the key sampleId1.

Useful articles