JavaScript Spread and Rest

The ... syntax does two opposite things: spread expands iterables into individual elements; rest collects elements into an array. Both are essential for clean, functional JavaScript.

Intermediate 9 min read 10 examples

Spread with Arrays

The spread operator (...) expands an iterable (array, string, Set, Map) into individual elements wherever a list of values is expected.

JavaScript
const nums = [1, 2, 3];
const more = [4, 5, 6];

// Expand into a new array
const all = [...nums, ...more];
console.log(all); // [1, 2, 3, 4, 5, 6]

// Insert in the middle
const inserted = [0, ...nums, 3.5, ...more];
console.log(inserted); // [0, 1, 2, 3, 3.5, 4, 5, 6]

// Spread a string into characters
const chars = [..."hello"];
console.log(chars); // ["h","e","l","l","o"]

// Spread an iterable (Set, Map)
const unique = [...new Set([1, 2, 2, 3, 3, 4])];
console.log(unique); // [1, 2, 3, 4]

// Spread in Math functions
const prices = [9.99, 14.99, 4.99, 24.99];
console.log(Math.max(...prices)); // 24.99
console.log(Math.min(...prices)); // 4.99

Spread with Objects

JavaScript
const defaults = { theme: "light", lang: "en", timeout: 5000 };
const userPrefs = { theme: "dark", lang: "fr" };

// Merge objects (right side wins on conflicts)
const config = { ...defaults, ...userPrefs };
console.log(config);
// { theme: "dark", lang: "fr", timeout: 5000 }

// Add/override while spreading
const updated = {
    ...defaults,
    theme: "dark",  // override
    newProp: true   // add
};

// Spread enumerates own enumerable properties
// Methods from prototype are NOT spread
class User { greet() {} }
const alice = new User();
alice.name = "Alice";
const plain = { ...alice };
console.log(plain);          // { name: "Alice" }
console.log(plain.greet);    // undefined (prototype method not included)

// Spread does not copy non-enumerable properties
const obj = Object.defineProperty({}, "hidden", { value: 42, enumerable: false });
console.log({ ...obj }); // {} (non-enumerable not included)

Cloning and Shallow Copy

JavaScript
// Shallow copy of an array
const original = [1, 2, 3, 4, 5];
const copy = [...original];
copy.push(6);
console.log(original); // [1, 2, 3, 4, 5] - unchanged
console.log(copy);     // [1, 2, 3, 4, 5, 6]

// Shallow copy of an object
const user = { name: "Alice", age: 28 };
const userCopy = { ...user };
userCopy.age = 30;
console.log(user.age);     // 28 - unchanged (primitives copied)
console.log(userCopy.age); // 30

// SHALLOW - nested objects are still shared!
const nested = { info: { score: 100 }, name: "Bob" };
const nestedCopy = { ...nested };
nestedCopy.info.score = 999; // modifies original!
console.log(nested.info.score); // 999 (shared reference!)

// Deep copy - use structuredClone (ES2022)
const deepCopy = structuredClone(nested);
deepCopy.info.score = 0;
console.log(nested.info.score); // 999 - original safe now

// Alternative: JSON round-trip (loses Dates, undefined, functions)
const jsonCopy = JSON.parse(JSON.stringify(nested));
Spread Creates Shallow Copies Only

The spread operator copies primitive values (numbers, strings) and copies references to nested objects. If you modify a nested object in the copy, the original is also affected. Use structuredClone() for deep cloning when nested objects must be independent.

Merging Arrays and Objects

JavaScript
// Array merging
const a = [1, 2, 3];
const b = [4, 5, 6];
const merged = [...a, ...b];          // [1,2,3,4,5,6]
const withExtra = [...a, 0, ...b];    // [1,2,3,0,4,5,6]
const reversed = [...b, ...a];        // [4,5,6,1,2,3]

// Remove duplicates while merging
const arr1 = [1, 2, 3];
const arr2 = [2, 3, 4, 5];
const uniqueMerge = [...new Set([...arr1, ...arr2])];
console.log(uniqueMerge); // [1, 2, 3, 4, 5]

// Object merging
const obj1 = { a: 1, b: 2 };
const obj2 = { b: 20, c: 30 }; // b conflicts
const merged2 = { ...obj1, ...obj2 };
console.log(merged2); // { a: 1, b: 20, c: 30 } (obj2.b wins)

// Immutable state update pattern (React/Redux)
const state = { count: 0, user: "Alice", theme: "light" };
const newState = { ...state, count: state.count + 1 };
console.log(state.count);    // 0 (original unchanged)
console.log(newState.count); // 1

// Conditionally include properties
const isAdmin = true;
const userObj = {
    name: "Alice",
    ...(isAdmin && { role: "admin", permissions: ["read", "write"] })
};
console.log(userObj); // includes role and permissions

Spread in Function Calls

JavaScript
function add(a, b, c) {
    return a + b + c;
}

const nums = [1, 2, 3];

// Old way: apply
console.log(add.apply(null, nums));  // 6

// Spread way (cleaner)
console.log(add(...nums));           // 6

// Mix spread with regular arguments
console.log(add(1, ...nums.slice(1))); // 6

// Passing array to Math functions
const scores = [95, 87, 92, 78, 88];
console.log(Math.max(...scores)); // 95
console.log(Math.min(...scores)); // 78

// Push multiple items into an array
const arr = [1, 2, 3];
const toAdd = [4, 5, 6];
arr.push(...toAdd);          // instead of arr.push(4, 5, 6) or concat
console.log(arr);            // [1, 2, 3, 4, 5, 6]

// Spread with new (you cannot use apply with new)
const dateArgs = [2024, 0, 15]; // Jan 15, 2024
const date = new Date(...dateArgs);
console.log(date.toDateString()); // "Mon Jan 15 2024"

Rest Parameters

Rest parameters collect remaining function arguments into a real Array. They must be the last parameter.

JavaScript
// Basic rest parameters
function sum(...numbers) {
    return numbers.reduce((total, n) => total + n, 0);
}
console.log(sum(1, 2, 3));         // 6
console.log(sum(1, 2, 3, 4, 5));   // 15
console.log(sum());                // 0

// First param fixed, rest collected
function log(level, ...messages) {
    const prefix = `[${level.toUpperCase()}]`;
    messages.forEach(msg => console.log(prefix, msg));
}
log("info", "Server started", "Listening on port 3000");
// [INFO] Server started
// [INFO] Listening on port 3000

// Rest is a real Array - can use all array methods
function joinWords(separator, ...words) {
    return words
        .map(w => w.trim())
        .filter(Boolean)
        .join(separator);
}
console.log(joinWords(", ", "Hello", "  World  ", "", "JS")); // "Hello, World, JS"

// Arrow functions with rest (no arguments object in arrow functions)
const multiply = (...nums) => nums.reduce((acc, n) => acc * n, 1);
console.log(multiply(2, 3, 4)); // 24

Spread vs Rest - At a Glance

ConceptSpreadRest
Symbol......
DirectionExpands (one to many)Collects (many to one)
ContextArray/object literals, function callsFunction parameters, destructuring
Result typeSpreads into surrounding contextAlways a real Array
PositionAnywhere in the expressionMust be last
Works withAny iterable (array, string, Set, Map)Function arguments
JavaScript
// SPREAD: ... in a value position (expands)
const arr = [1, 2, 3];
const copy = [...arr];       // spread in array literal
Math.max(...arr);            // spread in function call
const obj = { ...other };   // spread in object literal

// REST: ... in a pattern position (collects)
function fn(...args) {}      // rest in function parameters
const [a, ...rest] = arr;    // rest in array destructuring
const { x, ...others } = obj; // rest in object destructuring

Frequently Asked Questions

They use the same ... syntax but in opposite contexts. Spread expands an iterable (array, string, object) into individual elements - used in function calls, array literals, and object literals. Rest collects multiple elements into one array - used in function parameter lists and destructuring. Context determines which one: [...arr] = spread; (...args) in a function = rest.

No - spread creates a shallow copy. Top-level primitive values are copied, but nested objects and arrays are still shared by reference. Modifying a nested object in the copy also modifies the original. For a deep copy, use structuredClone(obj) (modern, handles most types), or JSON.parse(JSON.stringify(obj)) (does not handle Dates, undefined, functions, circular references).

Yes. Strings are iterable, so spreading a string splits it into individual characters: [..."hello"] gives ["h","e","l","l","o"]. This is a clean way to split a string into characters without str.split("").

Rest parameters (...args) collect remaining arguments into a real Array with all array methods. The legacy arguments object is array-like but NOT a real array - no map, filter, etc. Rest parameters only exist in regular functions (not arrow functions which have no arguments). Always prefer rest parameters over arguments in modern code.