Destructuring, spread and rest
Unpack arrays and objects into variables, set defaults and new names, and use spread and rest to copy, combine and collect values.
What you will learn
- Unpack arrays and objects with destructuring, including defaults and renaming
- Use spread to copy and combine arrays and objects
- Use rest to collect the remaining items or properties
- Merge user settings over defaults and say why the copy is shallow
On this page
Three pieces of syntax use the same dots and the same brackets to do opposite jobs. Destructuring pulls values out of arrays and objects into variables. Spread (...x in a call, array or object literal) pours values in. Rest (...x in a pattern or parameter list) collects what is left. All three copy references, not the data behind them. The difference between a value and a reference is explained in the fundamentals lesson on memory and references.
Array destructuring
The pattern on the left mirrors the shape of the array on the right. Position decides which item goes to which variable.
const rgb = [255, 128, 0];const [red, green, blue] = rgb;console.log(red, green, blue);
const [first, , third] = ["a", "b", "c"];console.log(first, third);
const [head, ...tail] = [1, 2, 3, 4];console.log(head, tail);
const [x = 10, y = 20] = [1];console.log(x, y);255 128 0a c1 [ 2, 3, 4 ]1 20Destructuring also swaps two variables, and it works on any iterable and on function results:
let a = 1;let b = 2;[a, b] = [b, a];console.log(a, b);
const [q, r] = "hi";console.log(q, r);
const [, second] = new Set(["p", "q"]);console.log(second);
console.log(Object.entries({ k: 1 }).map(([key, value]) => key + value));2 1h iq[ 'k1' ]- Leave a gap to skip an item:
const [first, , third]. ...tailin a pattern collects the remaining items into a new array. It must come last.= 10gives a default, used only when the item isundefined.[a, b] = [b, a]swaps two variables without a temporary. The line before it ends with a semicolon, which matters: without one, a line starting with[would continue the previous statement.- Destructuring works on any iterable, so a string and a
Setboth work.
Object destructuring
The pattern lists property names. Order does not matter.
const user = { name: "Ada", age: 36, address: { city: "London", zip: "N1" }, tags: ["admin", "dev"],};
const { name, age } = user;console.log(name, age);
const { name: userName, role = "guest" } = user;console.log(userName, role);Ada 36Ada guestPatterns nest, and ...rest collects the other properties:
const { address: { city }, tags: [firstTag] } = user;console.log(city, firstTag);
const { age: _age, ...rest } = user;console.log(rest);London admin{ name: 'Ada', address: { city: 'London', zip: 'N1' }, tags: [ 'admin', 'dev' ]}Defaults, parameters and assigning to existing variables:
const { nickname = "none", missing = 5, nothing = 7 } = { nickname: undefined, nothing: null,};console.log(nickname, missing, nothing);
function describe({ name, age = 0 }) { return `${name} (${age})`;}console.log(describe(user), describe({ name: "Linus" }));
let title;({ title } = { title: "Dr" });console.log(title);none 5 nullAda (36) Linus (0)Dr{ name, age }creates variables callednameandage.{ name: userName }reads the propertynameand stores it in a variable calleduserName. The name before the colon is the property, and the name after it is the new variable. This is the opposite of an object literal and is easy to mix up.role = "guest"is a default for a missing property.- Patterns nest:
address: { city }reaches into the nested object, andtags: [firstTag]reaches into the nested array. The namesaddressandtagsare not created as variables here. ...restcollects the other properties into a new object. Hereage: _agetakesageout of the way so thatresthas everything else.- A default applies only to
undefined.nicknamewasundefined, so it got"none".nothingwasnull, so it stayednull. - Parameters can be patterns too.
describe({ name, age = 0 })takes one object and unpacks it. That is the usual way to write a function with named options, as the parameters lesson showed. - To assign to existing variables, put the whole assignment in parentheses:
({ title } = obj). Without them, the braces look like a block.
Spread: copy and combine
In an array or object literal, ...x inserts the items or properties of x at that spot. In a function call, it passes the items as separate arguments.
const first = [1, 2];const second = [3, 4];console.log([...first, ...second, 5]);console.log([0, ...first]);console.log(Math.max(...first, ...second));console.log([..."abc"]);
const base = { a: 1, b: 2 };console.log({ ...base, c: 3 });console.log({ ...base, b: 20 });console.log({ b: 20, ...base });
console.log({ ...null, ...undefined, ...[9, 8] });
function total(label, ...numbers) { return `${label}: ${numbers.reduce((s, n) => s + n, 0)}`;}console.log(total("sum", 1, 2, 3));console.log(total("none"));[ 1, 2, 3, 4, 5 ][ 0, 1, 2 ]4[ 'a', 'b', 'c' ]{ a: 1, b: 2, c: 3 }{ a: 1, b: 20 }{ b: 2, a: 1 }{ '0': 9, '1': 8 }sum: 6none: 0[...first, ...second, 5]builds one array from two and a new item.Math.max(...first, ...second)callsmaxwith four arguments.[..."abc"]spreads any iterable into an array of its items.{ ...base, c: 3 }copies the properties ofbaseinto a new object and addsc.- Later wins.
{ ...base, b: 20 }overridesb.{ b: 20, ...base }letsbaseoverride it again, so the 2 comes back. - Spreading
nullorundefinedinto an object adds nothing. Spreading an array into an object copies its indexes as keys, which is rarely what you want. - The last two lines show rest parameters:
...numbersin a parameter list collects the extra arguments into a real array.
Spread into an array literal and slice() both give a shallow copy, which is the next point.
Merging settings over defaults
A common job is to start from defaults and apply what the user saved. Spread does it in one expression, but only one level deep.
const defaults = { theme: "light", fontSize: 14, editor: { tabWidth: 4, wrap: false },};
const saved = { fontSize: 16, editor: { wrap: true },};
const shallow = { ...defaults, ...saved };console.log(shallow);{ theme: 'light', fontSize: 16, editor: { wrap: true } }To merge the nested editor object as well, spread it inside the result:
const merged = { ...defaults, ...saved, editor: { ...defaults.editor, ...saved.editor },};console.log(merged);
console.log(defaults.editor);{ theme: 'light', fontSize: 16, editor: { tabWidth: 4, wrap: true } }{ tabWidth: 4, wrap: false }The first merge replaced the whole editor object, so tabWidth was lost. The second merge spreads the nested objects too, and keeps tabWidth: 4 from the defaults while taking wrap: true from the saved settings. The last line shows that the defaults were not touched: every spread made a new object, and nothing was assigned into defaults.
Spread copies one level. Nested objects and arrays are shared between the original and the copy, unless you spread them yourself. The next lesson looks at that in detail.
Common pitfalls
- Mixing up rename syntax.
{ name: userName }readsnameand createsuserName. - Expecting a default for
null. Defaults apply only toundefined. Use??for both. - Destructuring
undefinedornull.const { a } = undefinedthrows aTypeError. Give the value a fallback,const { a } = maybeObject ?? {}, or default the parameter. - Starting a line with
{or[after a line with no semicolon. The parser joins the lines. - Spreading an object where an array is expected, or the reverse. Objects are not iterable, so
[...obj]throws. - Thinking spread is a deep copy. Nested values are shared.
Exercises
A programme has defaults, a theme and the user options:
const defaults = { color: "red", size: { width: 1, height: 1 } };const theme = { size: { width: 5 } };const user = { color: "blue" };Build one merged object in which later layers win, and size is merged key by key instead of being replaced. Print merged, then defaults, to show that the defaults did not change. The output should be { color: 'blue', size: { width: 5, height: 1 } } and then { color: 'red', size: { width: 1, height: 1 } }.
Show solution
const defaults = { color: "red", size: { width: 1, height: 1 } };const theme = { size: { width: 5 } };const user = { color: "blue" };
const merged = { ...defaults, ...theme, ...user, size: { ...defaults.size, ...theme.size, ...user.size },};
console.log(merged);console.log(defaults);{ color: 'blue', size: { width: 5, height: 1 } }{ color: 'red', size: { width: 1, height: 1 } }The three top-level spreads make color blue, because user comes last. They would also make size equal to the theme’s { width: 5 }, so the last line of the literal replaces size with a merge of the three nested objects. Spreading user.size, which is undefined, adds nothing and is not an error. Every spread builds a new object, so defaults is untouched.
Write withoutPassword(account) that returns a copy of a record without its password, using a rest pattern in the parameter. Try it on { name: "Ada", email: "ada@example.com", password: "secret" }, then print the original too. The output should be { name: 'Ada', email: 'ada@example.com' } and then the full original record.
Show solution
function withoutPassword({ password, ...safe }) { return safe;}
const account = { name: "Ada", email: "ada@example.com", password: "secret" };console.log(withoutPassword(account));console.log(account);{ name: 'Ada', email: 'ada@example.com' }{ name: 'Ada', email: 'ada@example.com', password: 'secret' }The pattern { password, ...safe } takes password out as a variable and collects all other properties into a new object called safe. The function returns safe and ignores password. The original record is not changed, because destructuring only reads from it.
Given the record below, use one destructuring const statement to create status, first (the first item), others (the remaining items) and total. Print them on one line. The output should be 200 a 3 [ 'b', 'c' ].
const response = { status: 200, data: { items: ["a", "b", "c"], total: 3 },};Show solution
const response = { status: 200, data: { items: ["a", "b", "c"], total: 3 },};
const { status, data: { items: [first, ...others], total, },} = response;
console.log(status, first, total, others);200 a 3 [ 'b', 'c' ]The pattern follows the shape of the data. data: { ... } reaches into the nested record, items: [first, ...others] splits the array into its first item and the rest, and total is read from the same nested record. The names data and items are not created as variables.
Summary
- Destructuring unpacks arrays by position and objects by property name, with defaults (
= value), renaming (name: other) and nesting. - Rest (
...rest) collects what is left, in a pattern or in a parameter list. - Spread (
...x) inserts items into an array, object or call. Later values override earlier ones in objects. - Defaults apply to
undefinedonly, and spread copies only one level.
Next: what “copy” really means, and how to make a deep one.