Spread vs Rest Operators in JavaScript
Same three dots, two completely different jobs. JavaScript has this funny thing where the exact same syntax — ... (three dots) — does two opposite things depending on where you use it. In one context, it spreads values out. In another, it collects values together. When I first encountered this in the ChaiCode Web Dev Cohort 2026, I kept mixing them up. "Wait, is ... expanding or gathering here?" It took a few examples before the distinction clicked. And once it did, I started seeing both operators everywhere — in function parameters, array manipulation, object merging, you name it. Let me break it down the way I wish someone had explained it to me. Before we look at any syntax, understand this one concept: Spread = expanding a collection into individual pieces Rest = collecting individual pieces into a collection That's the entire difference. One unpacks, the other packs. SPREAD (expanding): [1, 2, 3] → 1, 2, 3 "Take this group and spread it out into individual items" REST (collecting): 1, 2, 3 → [1, 2, 3] "Take these individual items and gather them into a group" Both use ... — the difference is context. The spread operator takes an iterable (like an array or object) and spreads its contents out into individual elements. const original = [1, 2, 3]; const copy = [...original]; console.log(copy); // [1, 2, 3] console.log(copy === original); // false — it's a new, independent array Without spread, you might use original.slice() or Array.from(original). Spread is just cleaner. const fruits = ["Apple", "Mango"]; const veggies = ["Carrot", "Spinach"]; const food = [...fruits, ...veggies]; console.log(food); // ["Apple", "Mango", "Carrot", "Spinach"] Each array gets spread into individual elements, and they all land in the new array. No loops, no concat() — just clean, readable merging. const numbers = [2, 3, 4]; const extended = [1, ...numbers, 5, 6]; console.log(extended); // [1, 2, 3, 4, 5, 6] You can mix spread elements with regular values freely. const nums = [10, 20, 30]; Before spread: nums = [10, 20, 30] ← one package ...nums ↓ ↓ ↓ After spread: 10, 20, 30 ← individual values Usage: [...nums, 40] → [10, 20, 30, 40] Math.max(...nums) → Math.max(10, 20, 30) → 30 Spread works on objects too — it copies all key-value pairs into a new object. const user = { name: "Pratham", age: 22 }; const userCopy = { ...user }; console.log(userCopy); // { name: "Pratham", age: 22 } console.log(userCopy === user); // false — independent copy const defaults = { theme: "light", fontSize: 14, language: "en" }; const userPrefs = { theme: "dark", fontSize: 18 }; const settings = { ...defaults, ...userPrefs }; console.log(settings); // { theme: "dark", fontSize: 18, language: "en" } When properties collide, the last one wins. Here, userPrefs.theme ("dark") overwrites defaults.theme ("light"). This is a super common pattern for merging configurations. const product = { name: "Laptop", price: 75000 }; const updatedProduct = { ...product, price: 65000, inStock: true }; console.log(updatedProduct); // { name: "Laptop", price: 65000, inStock: true } The original product object stays untouched. You get a new object with the updated price and the new inStock property. You can spread an array as individual arguments to a function: const scores = [85, 92, 78, 95, 88]; // Without spread — awkward console.log(Math.max(85, 92, 78, 95, 88)); // 95 // With spread — clean console.log(Math.max(...scores)); // 95 console.log(Math.min(...scores)); // 78 Math.max() doesn't accept an array — it wants individual numbers. Spread converts the array into exactly that. The rest operator does the opposite of spread. It collects multiple individual elements into a single array or object. The most classic use case. When you don't know how many arguments a function will receive: const sum = (...numbers) => { let total = 0; for (const num of numbers) { total += num; } return total; }; console.log(sum(1, 2, 3)); // 6 console.log(sum(10, 20, 30, 40)); // 100 console.log(sum(5)); // 5 The ...numbers parameter collects all arguments into an array called numbers. You can pass any number of arguments and they'll all be gathered up. Function call: sum(10, 20, 30, 40) ↓ ↓ ↓ ↓ Parameter: (...numbers) ↓ ↓ ↓ ↓ numbers = [10, 20, 30, 40] ← collected into one array "Take all these individual values and pack them into an array" You can combine rest with named parameters — rest just has to come last: const introduce = (greeting, ...names) => { for (const name of names) { console.log(`${greeting}, ${name}!`); } }; introduce("Hello", "Pratham", "Arjun", "Priya"); // Hello, Pratham! // Hello, Arjun! // Hello, Priya! greeting gets the first argument ("Hello"), and ...names collects everything else into an array. We touched on this in the destructuring article, but it's worth repeating: const scores = [95, 88, 76, 82, 91]; const [first, second, ...rest] = scores; console.log(first); // 95 console.log(second); // 88 console.log(rest); // [76, 82, 91] first and second get their values by position. ...rest gathers everything that's left. const user = { name: "Pratham", age: 22, city: "Delhi", course: "Web Dev", isActive: true, }; const { name, age, ...details } = user; console.log(name); // "Pratham" console.log(age); // 22 console.log(details); // { city: "Delhi", course: "Web Dev", isActive: true } name and age are extracted. Everything else is collected into details. This is incredibly useful when you need to separate certain properties from the rest. Here's the thing that confuses everyone: they look identical. Both are .... The difference is entirely about where they appear. Feature Spread ... Rest ... What it does Expands/unpacks values Collects/gathers values Direction One → Many Many → One Where used Array literals, object literals, function calls Function parameters, destructuring Example [...arr], {...obj}, fn(...arr) (...args), [a, ...rest] Result Individual elements An array or object If ... appears on the right side of = (or in a function call), it's SPREAD. If ... appears on the left side of = (or in a function parameter), it's REST. // SPREAD — right side, expanding const newArr = [...oldArr]; // ^^^ spreading oldArr's values into newArr // REST — left side, collecting const [first, ...others] = someArray; // ^^^ collecting remaining values into others // SPREAD — in a function call, expanding console.log(Math.max(...numbers)); // ^^^ spreading array into individual arguments // REST — in a function parameter, collecting const sum = (...nums) => { /* ... */ }; // ^^^ collecting arguments into an array This is huge in React and modern JavaScript. Instead of modifying an object or array directly, you create a new one with spread: // Adding an item to an array without mutating const todos = ["Buy groceries", "Clean room"]; const newTodos = [...todos, "Study JavaScript"]; console.log(newTodos); // ["Buy groceries", "Clean room", "Study JavaScript"] console.log(todos); // ["Buy groceries", "Clean room"] — unchanged! // Updating an object property without mutating const user = { name: "Pratham", age: 22 }; const updatedUser = { ...user, age: 23 }; console.log(updatedUser); // { name: "Pratham", age: 23 } console.log(user); // { name: "Pratham", age: 22 } — unchanged! const logAll = (label, ...items) => { console.log(`--- ${label} ---`); items.forEach((item, i) => console.log(`${i + 1}. ${item}`)); }; logAll("My Skills", "JavaScript", "React", "Node.js", "MongoDB"); // --- My Skills --- // 1. JavaScript // 2. React // 3. Node.js // 4. MongoDB const frontend = ["HTML", "CSS", "JavaScript", "React"]; const backend = ["Node.js", "Express", "JavaScript", "MongoDB"]; const allSkills = [...new Set([...frontend, ...backend])]; console.log(allSkills); // ["HTML", "CSS", "JavaScript", "React", "Node.js", "Express", "MongoDB"] // JavaScript appears only once! const apiResponse = { id: 101, name: "Pratham", email: "[email protected]", }; // Add local metadata without touching the original response const enriched = { ...apiResponse, fetchedAt: new Date().toISOString(), source: "user-api", }; console.log(enriched); // { id: 101, name: "Pratham", email: "pratham@...", fetchedAt: "...", source: "user-api" } // This pattern is everywhere in React const Button = ({ label, onClick, ...otherProps }) => { console.log(label); // "Submit" console.log(onClick); // function console.log(otherProps); // { className: "btn-primary", disabled: false } }; Button({ label: "Submit", onClick: () => console.log("clicked"), className: "btn-primary", disabled: false, }); Rest destructuring lets you pull out the props you know about and forward the rest — a pattern you'll use constantly in React. const morningTasks = ["Exercise", "Breakfast", "Read news"]; const eveningTasks = ["Cook dinner", "Study", "Sleep"]; const allTasks = [...morningTasks, "Lunch break", ...eveningTasks]; console.log(allTasks); // ["Exercise", "Breakfast", "Read news", "Lunch break", "Cook dinner", "Study", "Sleep"] const baseConfig = { theme: "light", language: "en", notifications: true, }; const userConfig = { theme: "dark", fontSize: 18, }; const finalConfig = { ...baseConfig, ...userConfig }; console.log(finalConfig); // { theme: "dark", language: "en", notifications: true, fontSize: 18 } const average = (...numbers) => { const total = numbers.reduce((sum, n) => sum + n, 0); return total / numbers.length; }; console.log(average(80, 90, 70)); // 80 console.log(average(95, 88, 76, 82, 91)); // 86.4 const student = { name: "Pratham", age: 22, course: "Web Dev Cohort 2026", city: "Delhi", email: "[email protected]", }; const { name, email, ...academic } = student; console.log(name); // "Pratham" console.log(email); // "[email protected]" console.log(academic); // { age: 22, course: "Web Dev Cohort 2026", city: "Delhi" } Spread (...) expands an array or object into individual elements. Use it to copy, merge, and extend collections without mutating the original. Rest (...) collects multiple individual values into a single array or object. Use it in function parameters and destructuring to handle variable-length inputs. They look identical — the difference is context. Right side / function call = spread. Left side / function parameter = rest. Spread is essential for immutable updates — creating new arrays and objects instead of modifying existing ones. This is a core pattern in React. Both operators are used constantly in modern JavaScript. Array manipulation, object merging, flexible functions, destructuring, React components — they're everywhere. Three dots, two behaviors, infinite use cases. The spread and rest operators are one of those ES6 features that seem simple on the surface but fundamentally change how you write JavaScript. Once the "expanding vs collecting" mental model clicks, you'll find yourself reaching for ... in almost every file you write. I'm learning all of this through the ChaiCode Web Dev Cohort 2026 under Hitesh Chaudhary and Piyush Garg, and these operators come up in virtually every lesson — especially as we get into React where immutable state updates with spread are the norm. Getting comfortable with them now has already paid off. Connect with me on LinkedIn or visit PrathamDEV.in to follow along. More articles on the way. Happy coding! 🚀 Written by Pratham Bhardwaj | Web Dev Cohort 2026, ChaiCode
