Object and array destructuring
JavaScript destructuring, the syntax that unpacks options objects into the function signatures React components and Server Actions write later in the course.
Consider two bugs that share a root cause. In the first, a sendInvoice handler receives { customerEmail, amountCents, internalNotes } and forwards the whole object to email.send(input), so the customer-facing email now carries the admin-only notes meant for the finance team. Nothing at the call site mentions the notes, so nobody caught it. In the second, a React component writes const { name: customerName } = props to dodge an outer name binding but flips the rename direction: customerName is never created, and the local name shadows the wrong identifier. Both come from the same mistake, pulling fields out of an object without naming which ones, and in which direction.
The fix for both is destructuring. Destructuring is the consuming half of the options-object pattern from “Options objects”: the caller writes an object, and the function picks the fields it needs by name. From the React unit onward, components, Server Actions, and query helpers all take their input this way, and the course never re-explains the form.
Object destructuring and its four extensions
Section titled “Object destructuring and its four extensions”Here is the base form, applied to a small customer object we’ll reuse across this section:
const customer = { id: 'cust_123', name: 'Alex', email: 'alex@example.com', pageSize: 0,};
const { name, email } = customer;The field names sit on the left, the source object on the right. The line creates two local bindings, name and email, holding the values those fields had on the object. Each of the four extensions is this same shape with one piece added.
const { name: customerName } = customer;Rename. Reach for this when the outer scope already has a name binding the destructure would shadow, or when the local context wants a more specific name than the field’s. The direction reads backwards from the object-literal shorthand you might expect: left of the : is the field name on the source, right of the : is the local binding being created. After this line, customerName is in scope and name is not.
const { pageSize = 20 } = customer;Default. Reach for this when the field may be missing and you have a sensible substitute. The default follows the same rule as parameter defaults from “Options objects”: it fires only on undefined, never on null, 0, '', or false. On the customer object above, where pageSize is 0, the default does not fire; pageSize stays bound to 0, exactly as the caller intended. The language draws the line at missing, not falsy.
const { pageSize: limit = 20 } = customer;Combined rename + default. Reach for this when you want both a local name and a default. Read it in order: the rename happens first, then the default applies to the renamed binding (limit), not to the original field name (pageSize).
const { id, ...rest } = customer;Rest. This pulls id out by name and collects every remaining field into a new object bound to rest. Its most common use is omitting a field before forwarding the others, which the destructure-then-rebuild section below covers in depth. One syntactic rule: ...rest must be the last element in the pattern.
The rename trips first-time readers, so check your left-versus-right reading now, before it causes a shadowing bug in real code.
Given the line below, which name is now bound as a local variable, and which name was the original field on the source object?
const { foo: bar } = obj;foo is the new local binding; bar was the field name on obj.
bar is the new local binding; foo was the field name on obj.
Both foo and bar are now bound as local variables.
Neither is bound; this is a syntax error.
: local-binding. The field that existed on obj is foo; the variable the line creates in your scope is bar. This is the reverse of an object-literal shorthand, where { bar: foo } stores foo’s value under the key bar, which is why the rename is the most mis-read piece of destructuring syntax.Array destructuring: binding by position
Section titled “Array destructuring: binding by position”Object destructuring picks fields by name and ignores order; array destructuring binds by position. There is much less of it to learn.
const pair = ['admin', true];const [role, isActive] = pair;const [, isActive2] = pair;const [first, ...rest] = ['a', 'b', 'c'];The three lines show the three forms. The first binds each position to a variable in order. The second skips the first position with a leading comma and binds only the second. The third splits head from rest: it binds the first item and collects the remaining items into a new array, rest.
Reach for array destructuring only when the data is meaningfully ordered. Three cases fit: a useState return, where position 0 is the state and position 1 is the setter; an Object.entries pair, where position 0 is the key and position 1 is the value; an HTTP method-and-path tuple , where the order is part of the contract. On a plain array of records, position carries no meaning, so iterate and name each item instead.
Array destructuring is rarer than object destructuring, since most domain data is shaped as records and props rather than positions. You’ll reach for it in Unit 3, where every useState call returns a [value, setter] tuple you read this way.
Signature-level destructure: the options-object shape
Section titled “Signature-level destructure: the options-object shape”Every options-object function in this course writes one shape: the destructure happens at the parameter position, so the function unpacks its options object the moment the call arrives and the body reads the same identifiers the call site wrote.
const createInvoice = ({ customerId, amountCents, notes = '',}: { customerId: string; amountCents: number; notes?: string;}) => { // body reads the same names as the call site};Three payoffs make this the default shape:
- Names match the call site. The caller writes
createInvoice({ customerId, amountCents })and the body readscustomerIdandamountCents— noconst customerId = options.customerIdboilerplate, no remapping between call and implementation. - Adding a field never breaks existing callers. The destructure picks fields by name, so order is irrelevant, and a new optional field on the type adds a capability without touching call sites that don’t use it.
- Defaults live at the signature. Instead of opening the body with
const notes = options.notes ?? '', the default sits at the field, visible in the signature where a reviewer looks for the contract.
This is the shape every options-object function in the course uses, including React component props and Server Action input later on. Once it’s in your fingers, you’ll reach for it on every function past two parameters.
Signature destructure is the default for API-shaped functions. The alternative — destructuring on the first line of the body, with the parameter named options — is right only when the original options reference must stay in scope: to log the full payload, forward it to an SDK, or reference it more than once. Otherwise, reach for the signature form first.
Destructure-then-rebuild: the no-accidental-forwarding pattern
Section titled “Destructure-then-rebuild: the no-accidental-forwarding pattern”This is the discipline for using the form, and the structural fix for the wholesale-forward leak from the lesson’s first paragraph.
When forwarding data to a downstream call, destructure exactly the fields you need and rebuild the object literal that goes out. Never forward the original object wholesale.
The before and after share almost the same function; what differs is the data flowing out.
const sendInvoice = (input: SendInvoiceInput) => { db.insert(invoicesTable).values(input); email.send({ to: input.customerEmail, ...input });};sendInvoice receives { customerEmail, amountCents, internalNotes } and forwards the whole input to both callees. The database insert is fine; the full row belongs there. The email.send call is the leak: the customer-facing email now interpolates every field on input, including internalNotes. Nothing at the call site names internalNotes, which is exactly why a reviewer skimming the function misses it. And it decays: any admin-only field added to SendInvoiceInput upstream lands in the customer’s inbox too, with no change here.
const sendInvoice = ({ customerEmail, amountCents, internalNotes }: SendInvoiceInput) => { db.insert(invoicesTable).values({ customerEmail, amountCents, internalNotes }); email.send({ to: customerEmail, amountCents });};The signature names every field, and each downstream call gets an object literal built from those names and nothing else. email.send({ to: customerEmail, amountCents }) cannot carry internalNotes, because no one wrote it in. A new admin-only field on SendInvoiceInput tomorrow has no path to the email line. Nothing leaks unless a developer puts it there.
This is the options-object principle from “Options objects”, make the surface area visible at the call site, applied to a function’s outgoing side. The options object made the inputs explicit; the rebuilt literal makes the forwarded fields explicit. Data only flows where a developer has typed its name, so a field added elsewhere can’t silently reach a new place.
This is where the rest extension earns its keep: omit one field by name, forward everything else deliberately.
const { internalNotes, ...customerSafeFields } = invoice;return customerSafeFields;This is the canonical “strip a sensitive field” idiom, the shape a serializeInvoice helper takes before sending a record to the client, an email adapter takes before composing a customer-facing message, and any “send to third party” function takes when most of the record is safe but one field isn’t. The two directions call for different reaches: when picking a few fields, name them positively ({ customerEmail, amountCents }); when omitting one field from an otherwise-safe shape, the rest pattern is cleanest. The principle is the same either way.
Later chapters apply this discipline to the fields passed into Drizzle’s .values(…) call: only what was destructured by name reaches the table.
When nested destructuring throws
Section titled “When nested destructuring throws”Destructuring can nest: one pattern can reach two or three levels into an object. If a nullable link sits in the middle of that chain, the destructure throws at runtime.
const { profile: { address: { city } } } = user;This throws if user.profile or user.profile.address is nullish, because reaching city means dereferencing each level, and dereferencing undefined is a TypeError. The fix isn’t a cleverer nested destructure; it’s the ?. operator from the previous lesson:
const city = user.profile?.address?.city;Optional chaining steps through nullable links and short-circuits safely, and it reads more cleanly past the first level.
So the rule is: write shallow destructures by default and reach for ?. on nullable paths. Nested destructuring is legal, and occasionally the right call when the shape is guaranteed, as in a hardcoded config or a schema-validated parse. Past one level, though, the ?. form usually wins on both clarity and safety.
Sort each destructure by what it does
Section titled “Sort each destructure by what it does”Sort six snippets into three buckets, one for each piece of the model: syntactic validity (does the parser accept it?), semantic correctness (does the default fire when you expect?), and runtime safety (does it throw on the given input?).
Sort each destructure snippet by what it would do — given the input noted in the comment, where applicable. Drag each item into the bucket it belongs to, then press Check.
const { name, email } = customer;// Given: user.profile = nullconst { profile: { address } } = user;const { name = 'Anon' } = { name: '' };const { ...rest, id } = customer;const { id: , name } = customer;// Given: order.customer = undefinedconst { customer: { id } = {} } = order;From here on, every options-object function you write opens with a destructure that has to obey all three rules.
External resources
Section titled “External resources”The canonical reference for object and array destructuring — every form covered in this lesson and the corner cases beyond the experienced developer's daily reach.
The 'fires only on undefined' semantic that backs the field-level destructure default. Cross-reference for the rule you'll reach for at every signature.
The inline object-type annotation pattern used in the canonical signature-destructure shape, documented at language depth.
The build-time backstop for the rename-shadowing failure mode from the intro. Catches the rename-direction-flipped bug before it ships.