JS rest and spread in ES2015 / ES2018
By Christophe Porteneuve • Published on 14 February 2022
• 6 min
Cette page est également disponible en français.
Welcome to the fifth installment of our Idiomatic JS series.
In our article about destructuring, we tackled what I like to call the “Holy Trinity”: destructuring, rest/spread, and default values. In this one, we’ll cover rest/spread, and our next article in the series will cover the last member of the gang.
Most people were quick adopters of Rest / Spread as introduced in ES2015 (that is, on iterables only). Still, it’s all too easy to overlook some of its finer points, and too few developers routinely use the ES2018 extension to it that works on any object, making vanilla-JS cheap immutability a lot easier. Let’s review all of this together.
Rest and Spread: what are they for?
The idea is to pass a series of values (or key-value pairs) to an common container (e.g. Array
or a fresh object), or the other way around.
There are many use cases, not all of them immediately apparent, especially when you’re not a seasoned-enough JavaScript developers to have encountered all the relevant scenarios.
It’s a single operator for both roles: a ...
in prefix position (that is, before the identifier), e.g. ...items
. Whether it’s a rest or a spread depends on the location of that in the code:
- Within a declaration, it’s a rest. This could be a function signature or a destructuring. A rest always appears at the end of the list.
- Within an expression, it’s a spread. For instance arguments in a function call, values in an array literal or object literal. A spread can appear anywhere in the list (not just at the end), which can even contain multiple spreads.
Position-based rest
As the name implies, a rest lets you accumulate… the rest.
You can use it in function signatures or position-based destructurings ('told you these were best friends).
The goal is to accrue all remaining elements in the list in an actual Array
. This is useful mostly for two things:
- Implementing variadic functions, that is, functions that allow a variable number of arguments. This is an especially good fit for math- or aggregation-related functions (e.g. concatenation, combination towards a single result).
- Extracting the first items in an iterable (usually to apply a specific processing to them) and keeping the remaining items as a whole.
Let’s say you have a function that computes the average value of its arguments, regardless of their number. We definitely could use a loop, which could do with the good ol’ arguments
of traditional functions:
// So 1995… 😭
function oldAverage() {
var sum = 0
for (var index = 0, len = arguments.length; index < len; ++index) {
sum += arguments[index]
}
return sum / arguments.length
}
It might be neater to go with Array#reduce()
though. Except arguments
isn’t an Array
1, so tough luck. Not only that, our function signature doesn’t convey that it is variadic. Let’s try with a signature rest instead:
function newAverage(...values) {
return values.reduce((acc, n) => acc + n) / values.length
}
(1: it’s an instance of ~StupidCustomTypeFromHell
~Arguments
).
You should know that before rests, doing that in Array mode looked pretty wild 🔥:
function oldNewAverage() {
// 🤯😩
var values = Array.prototype.slice.call(arguments)
return values.reduce((acc, n) => acc + n) / values.length
}
As a bonus, rests can also be used for arrow functions, which isn’t true for arguments
(as we’ll see in our upcoming post about arrow functions #teaser #subscribe #goodForYourSkin).
We can also use that within position-based destructurings, as in the code below:
const [first, second, ...others] = ['Alice', 'Bob', 'Claire', 'David']
// first === 'Alice', second === 'Bob', others = ['Claire', 'David']
A position-based rest will always produce an array, even if it’s empty. There are no edge cases such as null
and undefined
, which is #chefskiss design, as edge cases are the worst.
Position-based spread
Position-based spread can be applied to any iterable, and consumes it entirely: it’s equivalent to putting, at that very code location, every single value in the iterable, separated by commas. You can use within function calls or an array literal.
As a reminder, tons of stuff (#jargon) are iterable: arrays naturally, but also NodeList
s (you know, the kind of thing document.querySelectorAll()
returns, among others), Map
s, Set
s, and even String
s (which is super cool).
For instance, this comes in handy when you have an array of arguments to be passed to a function, which expects them as individual arguments. This could let us use push
as a kind of mutative concat
:
const arr1 = [1, 2, 3]
const arr2 = [4, 5, 6]
arr1.concat(arr2) // => [1, 2, 3, 4, 5, 6]
// OK, but that didn't mutate arr1 tho:
arr1 // => [1, 2, 3]
// push is mutative, but it expects new values NOT as an array, but as
// individual arguments (push is variadic):
arr1.push(...arr2) // => 6 (new length)
arr1 // => [1, 2, 3, 4, 5, 6]
You can spread as many times as you need, including several times the same source, as a spread isn’t supposed to alter its source.
const arr1 = [1, 2, 3]
const arr2 = [5, 6, 7]
arr1.push(4, ...arr2, 8, ...arr2) // => 11
// => [1, 2, 3, 4, 5, 6, 7, 8, 5, 6, 7]
Position-based spread is also a neat way of deriving a new array from an original one (or any iterable for that matter):
const arr1 = [1, 2, 3]
[0, ...arr1, 4] // => [0, 1, 2, 3, 4]
As position-based spread works on iterables, applying it to anything else, including null
and undefined
, will throw a TypeError
.
Today’s protip
I see too many folks using position-based spread as the sole contents of a fresh array literal, in order to do a shallow copy of the array, or to convert an iterable to an actual array, like so:
// Yeah but meh
const newArray = [...originalArray]
// Still meh
const array = [...otherIterable]
So yes, ok, that does work, but you should favor the standard library API Array.from()
. Not only is it easier to read, it also lets you pass a mapping function that’ll be used on-the-fly, instad of doing a second traversal:
// #DidntReadTheDocs #NotSoRockStar #DoublePass
const mehWordSizes = [...wordsIterable].map((word) => word.length)
// 😍😎
const yeahWordSizes = Array.from(wordsIterable, (word) => word.length)
Name-based rest (ES2018)
ES2018 standardized a proposal titled Rest/Spread Properties, and that’s awesome.
Name-based rest is only possible within a name-based destructuring. It is quite similar in concept to a position-based rest within a destructuring: it will accrue all remaining key-value pairs from the source object, that is all the pairs from keys that you didn’t explicitly destructure earlier.
In the same way that a position-based rest will always produce an array, a name-based rest will always produce a new object, even if it ends up having zero own properties (an “empty” object that however retains Object
as prototype).
function preprocessConfig(config) {
const { debug, env, ...mainConfig } = config
// Here we use `debug` and `env`, that are relevant only to this function's code.
…
// Then we forward everything else to the main processing:
processConfig(mainConfig)
}
You also often find this in Higher-Order Components (HOC), a not-so-good approach in React before hooks came out in version 16.8. It would look something like this:
// Don't do this anymore!
function LoggingWrapper({ log, component: Component, ...delegated }) {
log.notify('rendering')
return <Component {...delegated} />
}
Name-based spread (ES2018)
Name-based spread can only be used within an object literal. You can use it multiple times, even from the same source. By virtue of its code location, it contributes to creating a new object: it doesn’t modify an existing one.
So it has different semantics than Object.assign()
.
A name-based spread is akin to listing at its code location all of the key-pair values for its operand’s enumerable own properties.
It has many cool uses, for instance shallow merging multiple objects towards a new one:
const addr1 = { street: '19 rue FM', zip: 92700, city: 'Colombes' }
const addr2 = { zip: 75011, city: 'Paris', country: 'FR' }
const addr = { ...addr1, ...addr2 }
// => { street: '19 rue FM', zip: 75011, city: 'Paris', country: 'FR' }
Or producing a final object from default properties, properties provided on the spot, and possible guaranteed properties in the end:
const defaults = { times: 1, separator: '-' }
function normalizeConfig(config) {
return { ...defaults, ...config, normalizedAt: Date.now() }
}
normalizeConfig({ term: 'yo', times: 4, normalizedAt: 0 })
// => { term: 'yo', times: 4, separator: '-', normalizedAt: 1639… }
It’s a neat way of producing a derived object from an existing one, instead of mutating it:
function derive(original, newSize) {
return { ...original, size: newSize }
}
const john = { name: 'John', age: 42, size: 5.8 }
derive(john, 6.1)
// => { name: 'John', age: 42, size: 6.1 }
john
// => { name: 'John', age: 42, size: 5.8 }
Just like position-based spread, it is therefore very handy in functional programming approaches that rely on immutability and value derivation.
As a final cool tip, it won’t balk when applied to null
or undefined
: it just won’t produce any key-value pairs. Nice.
There’s always more!
Remember there’s still a final member to our Holy Trinity! But we also have many wonderful topics left in this series.
Besides our wealth of other articles, you may wish to look at our live training courses! In particular, if you like super-deep dives into JavaScript itself, we heartily recommend our 360° ES course!