The spread operator in JavaScript is three dots that expand one value into the surrounding context. Inside an array literal it drops the elements in one by one; inside an object literal it copies the properties across; in a function call it turns an array into a list of separate arguments. It looks like one trick, but each position has its own rules, and two of them have sharp edges that will bite you if you have not met them yet.
Where does the spread operator work?
Four positions matter in ordinary application code. Inside an array literal it builds a new array from existing pieces. Inside an object literal it builds a new object from existing pieces. In an argument list it expands a value into separate arguments. And in a constructor call it does the same thing for new, which is handy for things like Date whose constructor takes separate numbers.
| Position | What you write | What it produces |
|---|---|---|
| array literal | [...items, 'last'] | a new array containing every element of items |
| object literal | { ...defaults, theme: 'dark' } | a new object with the copied properties |
| function call | greet(...args) | one argument per element of args |
| constructor call | new Date(...parts) | one argument per element of parts |
There is a fifth position that looks like the others but is not: the rest element, written ...rest in a destructuring pattern or a parameter list. Rest collects what is left into a new array. Spread pushes values out into the surrounding context. Same three dots, opposite directions, and mixing them up is a common source of confusion.
What does spreading into an array literal give you?
Every element, placed individually. That makes spread the natural way to combine collections without the nesting problem that concat can leave behind.
const base = ['read', 'write'];
const perms = [...base, 'delete'];
console.log(perms);
console.log([...'abc']);
console.log([...new Set(['a', 'b', 'a'])]);
console.log([...new Map([['id', 1]])]);
The first line gives a three-element array, not a two-element array with an array inside it. The second line spreads a string into its characters — and it splits by code point rather than by code unit, so a string containing an emoji produces one entry for the emoji rather than two broken halves. The third line turns a set into an array, which is how you get a de-duplicated list from something that was already a collection of unique values. The fourth line spreads a map into an array of key-and-value pairs, the same shape Object.entries produces.
Spreading only works on values that can be iterated: arrays, strings, sets, maps, and the array-like objects you meet occasionally. A plain object is not iterable, which is why [...someObject] throws rather than producing an array of its values.
How do you merge and override objects with spread?
Inside an object literal, spread copies the enumerable properties of the source object into the new one. Later sources win, so the order of the spread is the order of precedence — a habit worth stating in a comment when a config file is involved.
const defaults = { theme: 'light', fontSize: 14, lang: 'en' };
const userPrefs = { theme: 'dark', fontSize: 18 };
console.log({ ...defaults, ...userPrefs });
console.log({ ...userPrefs, ...defaults });
console.log({ ...defaults, theme: 'sepia' });
The first two lines are the same three properties in a different order, and the results differ: in the first, the user's preferences win; in the second, the defaults win and the user's dark theme is discarded. The third line shows the pattern for changing a single value — spread the object first, then write the property you want to change, so your value lands last and takes precedence.
Spreading null or undefined into an object literal is harmless: the result is an empty object rather than an error. That makes { ...maybeMissing } a safe way to build a base object from something that might not be there.
How does spread work in calls and constructors?
In an argument list, spread expands an array into separate arguments, filling the parameters from the left. The same expansion works for a constructor call.
function greet(name, punctuation) {
return `Hi ${name}${punctuation}`;
}
const args = ['Ada', '!'];
console.log(greet(...args));
console.log(Math.max(...[3, 9, 4]));
const parts = [2026, 2, 14];
const date = new Date(...parts);
console.log(date.getFullYear(), date.getMonth() + 1, date.getDate());
The Math.max line is the idiom you will meet most: Math.max takes a variable number of arguments and cannot take an array directly, so spread is how the array gets in. The Date line shows the constructor version, with the year, the month and the day supplied as three separate numbers from one array. Note that the month is zero-based in the date API, which is why the printed month is the value plus one.
The same expansion has a trap attached to it, because push accepts one argument and the array you give it is not expanded unless you ask.
const items = ['a', 'b'];
const copy = [...items];
copy.push('c');
console.log(items);
console.log(copy);
const batch = ['x', 'y'];
const target = [];
target.push(batch);
target.push(...batch);
console.log(target);
The first part shows that the spread copy is a genuinely separate array: adding to the copy leaves the original alone. The second part shows the difference between target.push(batch), which adds the array as a single element, and target.push(...batch), which adds each element. The output is an array containing one nested array followed by the two strings — and if you wanted three flat items, the second call is the one you need.
Where does spread bite?
Three sharp edges, in rough order of how often they catch people out.
const original = { name: 'Ada', prefs: { theme: 'light' } };
const copy = { ...original };
copy.prefs.theme = 'dark';
console.log(original.prefs.theme);
try {
console.log([...{ a: 1 }]);
} catch (err) {
console.log(err.constructor.name + ': ' + err.message);
}
try {
Math.max(...new Array(200000).fill(1));
} catch (err) {
console.log(err.constructor.name + ': ' + err.message);
}
First, the copy is shallow. The spread copied the reference to the nested prefs object, so changing copy.prefs.theme changes original.prefs.theme as well — the output says dark. Spreading protects the top level and nothing below it.
Second, spreading something that cannot be iterated throws. A plain object is not iterable, so [...{ a: 1 }] fails with a TypeError. This is the mirror image of the object-literal case, where { ...someObject } works perfectly: the two positions accept different kinds of value.
Third, every element becomes a real argument, and a call cannot take an unlimited number of them. Spreading two hundred thousand values into Math.max throws a RangeError about the maximum call stack size, because the engine has to build the argument list. For a list of any size where you are not certain, use a loop or a reduce instead of spreading.
Where spread belongs, and where it does not
- Array literals. Combine collections without nesting, and spread a set or map into an array when you need a list.
- Object literals. Copy and merge configuration in one line, with the later source taking precedence.
- Argument lists. Turn an array into separate arguments, and remember that the parameters fill from the left.
- Constructor calls. Supply separate values to a constructor that takes more than one.
- Not as a deep copy. Spread copies one level; anything nested is still shared.
- Not on very large lists in a call. The argument list has a limit, and the error is confusing when it is hit.
Try it yourself: write a function buildRequest(base, overrides) that returns a new request object made from a default object and a set of overrides, using spread so neither input is modified. Test it with a base of { method: 'GET', url: '/items', headers: { accept: 'application/json' }, timeout: 5000 } and overrides of { url: '/items?page=2', timeout: 15000 }, and confirm the returned object has the new URL and timeout while the base object still has the original ones. Then write the same function a second time with Object.assign and check that the two produce identical results. Finally, try spreading a list of ten thousand numbers into Math.max and then into a function that takes a single array, and notice which one works and why.