Saturday, March 06, 2021

Spread and Rest in JavaScript

What does the rest parameter syntax look like? 

Simple, It looks like this: ...stuff

That’s cool. What about the spread syntax? What does that look like?

Well, it also looks like this: ...stuff

And this is why the spread and the rest parameter syntax could be confusing and easily misplaced for each other. They have precisely the same syntax! What then separates them? Answer is the context they are used.

This post will look into these JavaScript syntaxes with the aim of disambiguating them. The hope is that it helps in removing the confusion.

The main tip to remember is this: the spread syntax is used to take a composite stricture (object or array) and spreads it out, while the rest syntax is used to take the rest of already spread out values (ie arguments to a function) and gather this into a composite structure.

The rest of the post expounds on the above statement.

Rest parameters syntax

The rest parameter syntax purpose is to simplify situations where one needs to take items of things and put that into an array. This is why one of the places rest parameter syntax is used is in the definition of variadic functions; these are functions that can be called with any number of arguments.

Another context in which rest parameter syntax is used is with the destructuring assignment. This is a process of taking composite values like arrays and objects, and unpacking the elements and assigning them to individual variables. 

Seeing examples will further make these two contexts in which rest parameter syntax more clear. Let’s start with defining variadic functions

Rest parameter syntax and variadic functions.

As an example, let’s imagine we need to define a function, concat, that takes any number of string arguments and concatenates them. Such a function should be useable like this:

concat("hello", "world") // "hello,world"
concat("rock", "the", "boat") // "rock,the,boat"
concat("live", "long", "and", "prosper") // "live,long,and,prosper"

This type of function is called a variadic function.

How may such a function be defined? 

We can not list all the parameters as we want to support any amount. Listing the parameters will constrict the functions to only the parameters we list.

We could define the function to take in an array as arguments. That helps in satisfying the requirements that any number of string can be concatenated, 

concat(["hello", "world"]) // "hello,world"
concat(["rock", "the", "boat"]) // "rock,the,boat"
concat(["live", "long", "and", "prosper"]) // "live,long,and,prosper"

but it changes the requirement: the requirement is to pass any number of arguments, and not an array.

It is for this sort of use case we can use the rest parameter syntax. Using the rest parameter, syntax such a function can be defined as this:

function concat(...args) {
  return args.join(",")
}

The rest parameter syntax, ie ...args will take all the arguments passed in, no matter the amount, and package it as a normal JavaScript array which can then be used within the function’s implementation.

Note, that it is also possible to define a function that takes a fixed set of arguments, followed by any number of arguments. 

As an example, imagine we need to update the concat function, such that the first argument should be a string separator, while the rest of the passed in arguments should be the strings to be concatenated together.

Such an implementation would look like this:

function concat(sep, ...args) {
   return args.join(sep)
}

And can be used thus:

concat(",", "hello", "world") // "hello,world"
concat("-", "hello", "world") // "hello-world"
concat(" ", "hello", "world") // "hello world"

This shows an important feature of the rest parameter syntax, which is, it can be used alongside a fixed amount of arguments: that is:

fun(arg1, ...rest) {} // good
fun(arg1, arg2, ...rest) {} // good
fun(arg1, arg2, arg3, ...rest) {} // good
fun(arg1, ...rest, arg2, arg3) {} // error

The only constraints are that the fixed number of arguments must come first, while the rest parameter syntax must be the last in the parameter list, and this is why it is called rest parameter syntax, as it allows taking the rest of the arguments a function is called with, and package that up into an array.

Rest parameter syntax and destructuring assignment 

As mentioned earlier, another place where rest parameter syntax can be used is in destructing assignments. 

What is destructuring assignment? How is that different from normal variable assignment?

Normal assignments take a value, and assign it, as is, to a variable. For example:

let arr = [1,2,3,4,5]
let obj = {
 name: "joe", 
 age: 20, 
 email: "joe@example.org", 
 isVerified: true
}

The above takes normal values, in this case, an array and an object, and assign, as is, to the variables: arr and obj.

Destructuring assignment, on the other hand, is different from the normal assignment. With a destructuring assignment, a value is taken apart, i.e. destructed, and its elements are what is assigned to variables.

For example we can destruct an array and then assign the individual elements to variables. This looks like this:

let [one, two, three, four, five] = [1,2,3,4,5]
console.log(one) // prints 1
console.log(two) // prints 2
console.log(three) // prints 3
console.log(four) // prints 4
console.log(five) // prints 5

We can also do the same to an object:

let {name, age, email, isVerified} = {
  name: "joe",
  age: 20,
  email: "joe@example.org",
  isVerified: true
}

console.log(name) // prints "Joe"
console.log(age) // prints 20
console.log(email) // prints joe@example.org
console.log(true) // prints 20

The rest parameter syntax can then be used for the situation where we only need to destruct part of the value being assigned while leaving the remaining values intact. 

For example, in the case of an array, if we only need to assign the first two values into individual elements while leaving the rest of the values as arrays, we will have:

let [one, two, ...remaining] = [1,2,3,4,5]
console.log(one) // prints 1
console.log(two) // prints 2
console.log(remaining) // prints [3,4,5]

Similarly, things can be achieved when dealing with objects. We could choose to destruct the object given above, and only individually assign the name property and age property to individual variables, while leaving the rest of the object. This will look like this:

let {name, age, ...remaining} = {
  name: "joe",
  age: 20,
  email: "joe@example.org",
  isVerified: true
}

// prints "Joe"
console.log(name)
// prints 20
console.log(age)
// prints {email: joe@example.org, isVerified: true}
console.log(remaining)

This is the general idea behind the rest parameter syntax, it provides the facility to gather up the rest of the elements and put that up into a composite structure: where the composite structure could be an array or an object.

Now that we have the rest parameter syntax covered, let us now look at the spread syntax.

Spread Syntax

The Spread syntax provides a succinct way to take a composite structure and spread the individual elements out. One can see it as doing the opposite of what the rest parameter syntax does.

When the spread syntax was initially added to JavaScript, the composite structure initially supported are iterables. But later on, in es2018, object literals were also added. By the way, if you do not know what iterables are, this post: Iterables and Iterators in JavaScript will help.

What exactly does: "take a composite structure (iterables or object literals) and spread the individual elements out" mean? This is best illustrated using code, so we quickly look at scenarios where one would want to take an iterable or object and spread out its individual elements. 

Note, JavaScript comes with a couple of inbuilt iterables, Arrays, Maps, Strings and the same spread syntax that will be shown will work for any other iterables: both inbuilt or custom: etc but for the proceeding examples, we would be making use of just Arrays. 

Extending an array or object with another

Let’s say we need to extend an array with another. This extension could either be via prepending, embedding, or appending the target array with another.

Taking prepending as an example, if you think about it, the act of prepending an array into another is, taking the array, and spreading its content out into the target array. 

In pseudo-code, the operation we need to perform is:

let arr = [1,2,3] let target = [4,5,6] let result = [spread out the contents of arr here, spread out the contents of target] result // this should now be [1,2,3,4,5,6]

The spread syntax gives us the ability to do exactly this. In real JavaScript code, this would look like this:

let arr = [1,2,3]
let target = [4,5,6]
let result = [...arr, ...target]
console.log(result) // prints [1,2,3,4,5,6]

The same also works for objects. We can prepend one object into another as we just did with arrays:

let first = {
  name: "joe",
  age: 20
}

let second = {
  email: "joe@example.org",
  isVerified: true
}

let full = {...first, ...second}

// will print
/*{
 name: "joe",
 age: 20,
 email: "joe@example.org",
 isVerified: true
}*/
console.log(full) 

This same approach can be used for appending, in that case, the source array is spread out after the target array.

Embedding involves writing out the contents of the target array and spreading in the source array in the position it is to be embedded in. For example:

let embed = [3, 4]
let oneToFive = [1, 2, ...embed, 5]
console.log(oneToFive) // [1,2,3,4,5]

Note that similar things can also be done with an array. That is:

let embed = {age: 20,  email: "joe@example.org"}

let person = {
 name: "joe",
 ...embed,
 isVerified: true
}

/*
will prints {
  name: "joe", 
  age: 20, 
  email: "joe@example.org", 
  isVerified: true
}
*/
console.log(person)

Cloning an array or object

Cloning can also be achieved by using the spread syntax. To clone an array you have:

let arr = [1,2,3]
let clonedArr = [...arr]
console.log(clonedArr) // prints [1,2,3]

Objects can also be cloned in a similar way

let obj = {name: "Joe", age: "20"}
let clonedObj = {...obj}
console.log(clonedObj) // prints {name: "Joe", age: "20"}

If you think about it, the act of cloning is spreading out the contents of something like an array or object and have it copied over into another array/object.

Calling a function

It is also possible to use the spread syntax when calling a function. To illustrate this, let us assume we have a simple function that joins 2 strings together

function join(input1,input2) {
  return input1 + input2;
}

This function can be called normally:

join("sun", "shine") // "sunshine"

But it is also possible given an array, to use the spread syntax, to extract the elements of that array and pass them on to the sum function. This will look like this:

let input = ["sun", "shine"]
join(...input) // "sunshine"

This works, because the spread syntax, spreads out the content of the array and makes them as arguments to the functions.

Note that if the passed-in array is longer than the number of arguments required to call the function, the spread syntax will only extract and use as much of the elements needed to call the function and ignore the remaining. For example:

let input = ["sun", "shine", "how", "you", "doing"]

join(...input) // "sunshine"

The join function only needs two arguments, hence the spread syntax only uses the first two elements in the array. 

Summary

  • Rest syntax collects multiple elements into a single element
  • Spread syntax "expands" a single element into its multiple elements
  • Rest parameter syntax can be used when defining variadic functions
  • Rest parameter syntax can be used in Destructing assignments
  • While The spread syntax is used when calling a function




I am writing a book: TypeScript Beyond The Basics. Sign up here to be notified when it is ready.

No comments: