DEV Community

Harman Panwar
Harman Panwar

Posted on

Spread vs Rest Operators in JavaScript

Spread and Rest Operators in JavaScript

JavaScript's spread and rest operators use the same syntax: three dots (...). At first glance, this can be confusing—how can one syntax do two different things? The key is context. When you use ... on the right side of an assignment or in a function call, it's the spread operator. When you use it on the left side of an assignment or in function parameters, it's the rest operator. This guide will clarify both and show you practical ways to use them.

What the Spread Operator Does

The spread operator expands an iterable (like an array or object) into individual elements. Think of it as "unpacking" a collection—wherever you write ...myArray, JavaScript replaces it with each element from that array.

const numbers = [1, 2, 3];
console.log(...numbers);  // Output: 1 2 3
Enter fullscreen mode Exit fullscreen mode

This is most useful when you need to pass the elements of an array as separate arguments to a function:

const numbers = [1, 2, 3];

// Without spread - passes the whole array as one argument
console.log(Math.max(numbers));       // Output: NaN

// With spread - passes each element as separate arguments
console.log(Math.max(...numbers));    // Output: 3
Enter fullscreen mode Exit fullscreen mode

The spread operator works with any iterable, including strings. You can spread a string to get individual characters:

const greeting = "hello";
console.log(...greeting);  // Output: h e l l o
Enter fullscreen mode Exit fullscreen mode

It also works with Sets, which are iterable collections:

const uniqueNumbers = new Set([1, 2, 2, 3, 3]);
console.log(...uniqueNumbers);  // Output: 1 2 3
Enter fullscreen mode Exit fullscreen mode

What the Rest Operator Does

The rest operator collects multiple elements into a single array or object. It's the opposite of spread—it packs elements together instead of unpacking them.

The rest operator appears in two main contexts: function parameters and destructuring assignments.

In Function Parameters

When used in function parameters, rest collects all passed arguments into an array:

function sum(...numbers) {
  return numbers.reduce((total, num) => total + num, 0);
}

console.log(sum(1, 2, 3));        // Output: 6
console.log(sum(1, 2, 3, 4, 5));  // Output: 15
console.log(sum());               // Output: 0
Enter fullscreen mode Exit fullscreen mode

The function doesn't need to know how many arguments will be passed—it just collects whatever comes in.

In Destructuring

Rest also works when destructuring arrays or objects:

// Rest with arrays
const [first, second, ...rest] = [1, 2, 3, 4, 5];
console.log(first);   // Output: 1
console.log(second);  // Output: 2
console.log(rest);   // Output: [3, 4, 5]

// Rest with objects
const { name, age, ...others } = { name: "Alice", age: 30, city: "NYC", country: "USA" };
console.log(name);    // Output: "Alice"
console.log(age);     // Output: 30
console.log(others);  // Output: { city: "NYC", country: "USA" }
Enter fullscreen mode Exit fullscreen mode

Key Differences Between Spread and Rest

The difference comes down to direction and purpose:

Aspect Spread Operator Rest Operator
Purpose Expand/unwrap Collect/pack
Direction Right side of assignment Left side of assignment
Location Arrays in function calls, object literals Function parameters, destructuring
Result Individual elements Array of elements

Think of spread as "spreading out" (expanding) and rest as "gathering the rest" (collecting).

// SPREAD - unpacking
const a = [1, 2];
const b = [3, 4];
const combined = [...a, ...b];  // [1, 2, 3, 4]

// REST - packing
function printAll(...args) {
  console.log(args);  // Array of all arguments
}
Enter fullscreen mode Exit fullscreen mode

Using Spread with Arrays

The spread operator makes working with arrays much cleaner. Here are the most common patterns.

Combining Arrays

Instead of using concat() or push(), you can merge arrays with spread:

const fruits = ["apple", "banana"];
const vegetables = ["carrot", "lettuce"];

// Combine two arrays
const produce = [...fruits, ...vegetables];
console.log(produce);  // ["apple", "banana", "carrot", "lettuce"]

// Add elements before and after
const moreProduce = ["tomato", ...fruits, "cucumber", ...vegetables];
console.log(moreProduce);  // ["tomato", "apple", "banana", "cucumber", "carrot", "lettuce"]
Enter fullscreen mode Exit fullscreen mode

Copying Arrays

Spread creates a shallow copy of an array. This is useful when you want to create a modified version without changing the original:

const original = [1, 2, 3];
const copy = [...original];

console.log(copy);      // [1, 2, 3]
copy.push(4);
console.log(original);  // [1, 2, 3] - unchanged
console.log(copy);      // [1, 2, 3, 4]
Enter fullscreen mode Exit fullscreen mode

Important: spread only creates a shallow copy. If your array contains objects, the objects themselves are still shared:

const original = [{ name: "Alice" }, { name: "Bob" }];
const copy = [...original];

copy[0].name = "Charlie";
console.log(original[0].name);  // "Charlie" - nested object is shared!
Enter fullscreen mode Exit fullscreen mode

Converting to Array

Strings and other iterables can be converted to arrays using spread:

const greeting = "hello";
const letters = [...greeting];
console.log(letters);  // ["h", "e", "l", "l", "o"]

// Works with any iterable
const set = new Set([1, 2, 3]);
const setToArray = [...set];
console.log(setToArray);  // [1, 2, 3]
Enter fullscreen mode Exit fullscreen mode

Removing Duplicates

Combined with Set, spread provides an easy way to remove duplicates:

const withDuplicates = [1, 2, 2, 3, 3, 3, 4];
const unique = [...new Set(withDuplicates)];
console.log(unique);  // [1, 2, 3, 4]
Enter fullscreen mode Exit fullscreen mode

Using Spread with Objects

The spread operator works with objects too, which is particularly useful for creating modified copies.

Copying Objects

Just like arrays, objects can be shallow copied:

const person = { name: "Alice", age: 30 };

// Create a copy
const personCopy = { ...person };
console.log(personCopy);  // { name: "Alice", age: 30 }

// Modify the copy without affecting original
personCopy.city = "NYC";
console.log(person);      // { name: "Alice", age: 30 }
console.log(personCopy);   // { name: "Alice", age: 30, city: "NYC" }
Enter fullscreen mode Exit fullscreen mode

Merging Objects

Object spread lets you merge multiple objects:

const defaults = { theme: "dark", language: "en" };
const userPrefs = { theme: "light" };

// User preferences override defaults
const settings = { ...defaults, ...userPrefs };
console.log(settings);  // { theme: "light", language: "en" }
Enter fullscreen mode Exit fullscreen mode

When there's a conflict, the later object wins. In this example, theme: "light" from userPrefs overrides theme: "dark" from defaults.

Adding or Overriding Properties

Spread makes it easy to create modified versions of objects:

const user = { name: "Alice", age: 30, city: "NYC" };

// Add new property
const userWithEmail = { ...user, email: "alice@example.com" };

// Override existing property
const olderUser = { ...user, age: 31 };

// Reorder properties (note: name becomes last)
const reordered = { city: user.city, name: user.name, age: user.age };
Enter fullscreen mode Exit fullscreen mode

Practical Example: Form Updates

A common pattern in React or similar frameworks is updating part of an object:

const formState = {
  name: "Alice",
  email: "alice@example.com",
  password: "secret123"
};

// User changes their name
const updatedForm = {
  ...formState,
  name: "Alice Smith"
};
Enter fullscreen mode Exit fullscreen mode

Practical Use Cases

Let's walk through real-world scenarios where spread and rest prove invaluable.

Flexible Function Arguments

Rest allows functions to accept any number of arguments:

function logMessages(...messages) {
  messages.forEach((msg, i) => {
    console.log(`${i + 1}. ${msg}`);
  });
}

logMessages("Hello", "World", "!");
// Output:
// 1. Hello
// 2. World
// 3. !
Enter fullscreen mode Exit fullscreen mode

Collecting Remaining Properties

When destructuring, rest captures everything else:

function parseUser({ name, age, ...details }) {
  console.log(`${name} is ${age} years old`);
  console.log("Additional info:", details);
}

parseUser({ name: "Alice", age: 30, city: "NYC", country: "USA" });
// Output:
// Alice is 30 years old
// Additional info: { city: "NYC", country: "USA" }
Enter fullscreen mode Exit fullscreen mode

Passing Array as Arguments

Convert an array of values into separate function arguments:

const numbers = [3, 1, 4, 1, 5, 9, 2, 6];

// Math.min expects separate arguments, not an array
console.log(Math.min(...numbers));      // Output: 1
console.log(Math.max(...numbers));      // Output: 9

// Compare to without spread
console.log(Math.min(numbers));         // Output: NaN
Enter fullscreen mode Exit fullscreen mode

Object Updates Without Mutation

Create new objects with specific changes:

const state = {
  users: [],
  filter: "all",
  page: 1
};

// Update without changing original
const newState = {
  ...state,
  page: 2,
  filter: "active"
};
Enter fullscreen mode Exit fullscreen mode

Function Composition

Build higher-order functions that accept any number of transformations:

function pipe(...functions) {
  return function(value) {
    return functions.reduce((result, fn) => fn(result), value);
  };
}

const addOne = x => x + 1;
const double = x => x * 2;
const square = x => x * x;

const transform = pipe(addOne, double, square);

console.log(transform(3));  // (3 + 1) * 2 = 8, 8 * 8 = 64
Enter fullscreen mode Exit fullscreen mode

Replacing apply Calls

Before arrow functions, you'd use Function.prototype.apply to spread arrays as arguments:

// Old way with apply
const max = Math.max.apply(null, [1, 2, 3, 4, 5]);

// Modern way with spread
const maxModern = Math.max(...[1, 2, 3, 4, 5]);
Enter fullscreen mode Exit fullscreen mode

The spread syntax is cleaner and more readable.

Combining Default Config with User Config

A common pattern for configuration:

const defaultConfig = {
  timeout: 5000,
  retries: 3,
  apiKey: "default-key"
};

function makeRequest(userConfig = {}) {
  const config = { ...defaultConfig, ...userConfig };
  console.log(config);
}

makeRequest();  // Uses all defaults
makeRequest({ timeout: 2000 });  // Only overrides timeout
Enter fullscreen mode Exit fullscreen mode

Common Mistakes to Avoid

Understanding the difference between spread and rest helps avoid confusion.

Trying to spread a non-iterable:

// This fails - objects aren't iterable by default
const obj = { a: 1 };
// console.log(...obj);  // Error!

// But you can spread an object's own properties into another object
const newObj = { ...obj };
Enter fullscreen mode Exit fullscreen mode

Confusing location:

// Spread (right side) - unpacks
const arr = [1, 2];
const expanded = [...arr];  // [1, 2]

// Rest (left side) - packs
function fn(...args) { }  // args is an array

// In destructuring:
const [first, ...remaining] = [1, 2, 3];  // remaining is [2, 3]
Enter fullscreen mode Exit fullscreen mode

Forgetting order matters with objects:

const obj1 = { a: 1, b: 2 };
const obj2 = { b: 3, c: 4 };

// Later values override earlier ones
const merged = { ...obj1, ...obj2 };
console.log(merged);  // { a: 1, b: 3, c: 4 }
Enter fullscreen mode Exit fullscreen mode

Suggestions for Using Spread and Rest

Prefer spread for creating modified copies. Instead of mutating arrays or objects directly, use spread to create new versions:

// Instead of
arr.push(newItem);

// Use
const newArr = [...arr, newItem];
Enter fullscreen mode Exit fullscreen mode

Use rest for variadic functions. Functions that accept any number of arguments should use rest parameters:

// Instead of
function sum() { return Array.from(arguments).reduce(...); }

// Use
function sum(...numbers) { return numbers.reduce(...); }
Enter fullscreen mode Exit fullscreen mode

Use destructuring with rest for cleaner parameter handling. Instead of accessing properties individually:

// Instead of
function handleUser(user) {
  const name = user.name;
  const age = user.age;
  const email = user.email;
}

// Use
function handleUser({ name, age, email }) {
  // Direct access to name, age, email
}
Enter fullscreen mode Exit fullscreen mode

Keep the order logical. When merging objects, think about which values should take precedence:

const merged = { ...defaults, ...overrides };
Enter fullscreen mode Exit fullscreen mode

Use spread for immutability patterns. In React and similar frameworks, spreading helps maintain immutability:

setState(prevState => ({
  ...prevState,
  newField: newValue
}));
Enter fullscreen mode Exit fullscreen mode

Remember shallow vs deep copy. Spread creates shallow copies—both original and copy share references to nested objects. For deep copies, you'll need a library or JSON.parse(JSON.stringify()) (though that has limitations with functions and special values).

Quick Reference

Operation Code Result
Copy array [...arr] New array with same elements
Copy object { ...obj } New object with same properties
Merge arrays [...arr1, ...arr2] Combined array
Merge objects { ...obj1, ...obj2 } Combined object
Function rest function(...args) args is array of all arguments
Destructure rest const [a, ...rest] = arr rest is remaining elements

Conclusion

The spread and rest operators, despite using identical syntax (...), serve opposite purposes. Spread expands iterables into individual elements—it's the "unpack" operation. Rest collects multiple elements into a single array—it's the "pack" operation.

Understanding the context determines which you're using: on the right side of an assignment or in function calls, it's spread; on the left side of an assignment or in function parameters, it's rest. Once this distinction clicks, you'll find yourself using these operators constantly for cleaner, more expressive code.

The practical applications are everywhere: combining arrays and objects, creating copies without mutation, accepting flexible arguments in functions, and handling unknown numbers of parameters gracefully. Both operators are fundamental tools that will make your JavaScript code more concise and maintainable.

Top comments (0)