Understanding Destructuring, Spread, and Rest Parameters

Since ES2015, JavaScript has introduced several powerful features for working with objects and arrays: destructuring assignment, the spread operator, and rest parameters.

At first glance, these syntaxes may seem unrelated, but they all revolve around the same idea: making it easier to extract, combine, and transform data structures.

In this note, we'll:

  • Understand destructuring, spread, and rest in a unified way.
  • Go through practical examples you'll use frequently.
  • Highlight some common pitfalls, such as shallow copying and property overwrites.

1. Destructuring Assignment

Destructuring assignment allows you to extract values from objects or arrays and assign them directly to variables.

1.1 Object Destructuring

Consider the following object:

const note = {
  id: 1,
  title: "My first note",
  date: "01/01/1970",
}

Traditionally, you'd write:

const id = note.id
const title = note.title
const date = note.date

With object destructuring:

const { id, title, date } = note

console.log(id) // 1
console.log(title) // "My first note"
console.log(date) // "01/01/1970"

Note: Destructuring does not modify the original object.

Renaming Variables During Destructuring

Sometimes you want a different variable name:

const { id: noteId, title, date } = note

console.log(noteId) // 1

Here, noteId receives the value of note.id.

Destructuring Nested Objects

Let's add an author object:

const note = {
  id: 1,
  title: "My first note",
  date: "01/01/1970",
  author: {
    firstName: "Sherlock",
    lastName: "Holmes",
  },
}

You can destructure nested properties directly:

const {
  id,
  title,
  date,
  author: { firstName, lastName },
} = note

console.log(`${firstName} ${lastName}`)
// "Sherlock Holmes"

If you want both the entire author object and its properties:

const {
  author,
  author: { firstName, lastName },
} = note

console.log(author)
// { firstName: "Sherlock", lastName: "Holmes" }

Destructuring Primitive Values

Even primitive values can be destructured through their wrapper objects:

const { length } = "A string"

console.log(length) // 8

JavaScript temporarily wraps the string in a String object to access its properties.


1.2 Array Destructuring

Array destructuring works by position:

const date = ["1970", "12", "01"]

const [year, month, day] = date

console.log(year) // "1970"
console.log(month) // "12"
console.log(day) // "01"

Skipping Elements

You can skip unwanted values:

const [year, , day] = date

console.log(year) // "1970"
console.log(day) // "01"

Nested Array Destructuring

const nestedArray = [1, 2, [3, 4], 5]

const [one, two, [three, four], five] = nestedArray

console.log(one, two, three, four, five)
// 1 2 3 4 5

Destructuring Function Parameters

You'll often see destructuring directly in function parameters:

const note = {
  id: 1,
  title: "My first note",
  date: "01/01/1970",
}

Object.entries(note).forEach(([key, value]) => {
  console.log(`${key}: ${value}`)
})

// Or using for...of
for (const [key, value] of Object.entries(note)) {
  console.log(`${key}: ${value}`)
}

Output:

id: 1
title: My first note
date: 01/01/1970

Combining Defaults, Objects, and Arrays

Default values can be mixed with object and array destructuring:

const note = {
  title: "My first note",
  author: {
    firstName: "Sherlock",
    lastName: "Holmes",
  },
  tags: ["personal", "writing", "investigations"],
}

const {
  title,
  date = new Date(),
  author: { firstName },
  tags: [personalTag, writingTag],
} = note

console.log(date)

If date doesn't exist on note, the current date will be used.


2. The Spread Operator (...)

The spread operator expands an iterable or object into individual elements or properties.

Common use cases include:

  • Merging arrays
  • Copying arrays or objects
  • Passing arguments to functions

2.1 Spread with Arrays

Combining arrays:

const tools = ["hammer", "screwdriver"]
const otherTools = ["wrench", "saw"]

const allTools = [...tools, ...otherTools]

console.log(allTools)
// ["hammer", "screwdriver", "wrench", "saw"]

Adding an item without mutating the original array:

const users = [
  { id: 1, name: "Ben" },
  { id: 2, name: "Leslie" },
]

const newUser = { id: 3, name: "Ron" }

const updatedUsers = [...users, newUser]

console.log(users)
console.log(updatedUsers)

Reference Copy vs Shallow Copy

Reference copy:

const originalArray = ["one", "two", "three"]
const secondArray = originalArray

secondArray.pop()

console.log(originalArray)
// ["one", "two"]

Shallow copy using spread:

const originalArray = ["one", "two", "three"]
const secondArray = [...originalArray]

secondArray.pop()

console.log(originalArray)
// ["one", "two", "three"]

Converting a Set or string into an array:

const set = new Set()
set.add("octopus")
set.add("starfish")
set.add("whale")

const seaCreatures = [...set]
console.log(seaCreatures)

const string = "hello"
const stringArray = [...string]
console.log(stringArray)

2.2 Spread with Objects

Creating a shallow copy:

const originalObject = {
  enabled: true,
  darkMode: false,
}

const secondObject = { ...originalObject }

console.log(secondObject)

Adding or overriding properties:

const user = {
  id: 3,
  name: "Ron",
}

const updatedUser = {
  ...user,
  isLoggedIn: true,
}

console.log(updatedUser)

Updating Nested Objects

Be careful with nested objects:

const user = {
  id: 3,
  name: "Ron",
  organization: {
    name: "Parks & Recreation",
    city: "Pawnee",
  },
}

Incorrect:

const brokenUser = {
  ...user,
  organization: {
    position: "Director",
  },
}

This replaces the entire organization object.

Correct:

const updatedUser = {
  ...user,
  organization: {
    ...user.organization,
    position: "Director",
  },
}

2.3 Spread in Function Calls

Spread can unpack arrays into function arguments:

function multiply(a, b, c) {
  return a * b * c
}

const numbers = [3, 2, 1]

console.log(multiply(...numbers))
// 6

Before spread syntax, you'd typically use apply:

multiply.apply(null, numbers)
// 6

3. Rest Parameters (...)

Although rest parameters use the same ... syntax, they do the opposite of spread.

  • Spread: break values apart.
  • Rest: collect values together.

3.1 Rest Parameters in Functions

Accepting an arbitrary number of arguments:

function restTest(...args) {
  console.log(args)
}

restTest(1, 2, 3, 4, 5, 6)
// [1, 2, 3, 4, 5, 6]

Collecting only the remaining arguments:

function restTest(one, two, ...args) {
  console.log(one) // 1
  console.log(two) // 2
  console.log(args) // [3, 4, 5, 6]
}

restTest(1, 2, 3, 4, 5, 6)

Rest vs Arguments

Traditional JavaScript uses arguments:

function testArguments() {
  console.log(arguments)
}

testArguments("how", "many", "arguments")

Limitations of arguments:

  • Not available in arrow functions.
  • Not a real array.
  • Always captures all arguments.

Rest parameters avoid all of these issues.

3.2 Rest in Destructuring

Array destructuring:

const [firstTool, ...rest] = [
  "hammer",
  "screwdriver",
  "wrench",
]

console.log(firstTool)
// "hammer"

console.log(rest)
// ["screwdriver", "wrench"]

Object destructuring:

const { isLoggedIn, ...rest } = {
  id: 1,
  name: "Ben",
  isLoggedIn: true,
}

console.log(isLoggedIn)
// true

console.log(rest)
// { id: 1, name: "Ben" }

This pattern is extremely common when you only care about a few properties and want to pass the rest elsewhere.


4. Summary & Personal Usage Guidelines

To keep things simple and avoid common mistakes, here are a few habits I follow:

Destructuring

  • Destructure props, API responses, and configuration objects as early as possible.
  • Avoid overly deep nested destructuring. If it goes beyond two levels, consider extracting variables separately.

Spread

  • Prefer creating new arrays or objects instead of mutating existing references.
  • Remember that spread performs a shallow copy.
  • When updating nested objects, spread both the parent and child objects.

Rest

  • Use rest parameters for utility functions, logging helpers, and APIs that accept variable numbers of arguments.
  • Use rest in destructuring to collect remaining properties, such as:
const { className, ...rest } = props

This is especially common in React component development.