Skip to content
Chapter 2Lesson 6

Destructuring as the API call-shape

JavaScript destructuring, the syntax that unpacks options objects into the function signatures every React component and Server Action later in the course will write.

Consider two short bugs that share a root cause. In the first, a sendInvoice handler receives an object, { customerEmail, amountCents, internalNotes }, and forwards the whole thing to email.send(input). The customer-facing email now includes the internal admin-only notes meant for the finance team, and nobody noticed because nothing at the call site mentions the notes. In the second, a React component writes const { name: customerName } = props to avoid clashing with an outer name binding, but the author flips the rename direction by mistake. The renamed customerName is never created, and the local name shadows the wrong identifier. Both bugs come from the same place: a function pulled fields out of an object without naming exactly which ones, and in which direction.

The fix for both is destructuring. Destructuring is the consuming half of the options-object pattern from the previous lesson “Signatures that stay readable past two parameters”: the caller writes an object, and the function picks the fields it needs by name. Once you’re past the React unit, every component, every Server Action, and every Drizzle query helper takes its input this way, and the course never re-explains the form.

The lesson moves through four topics. First, object destructuring and its four extensions: rename, default, combined, and rest. Then array destructuring and the two places you reach for it daily. Then the signature-level destructure, which is the canonical Server Action and React-prop shape. Finally, the destructure-then-rebuild pattern, which is the structural fix for the wholesale-forward leak.

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;

That single line is the whole base form: two const assignments by key, written as one expression. The field names sit on the left and the source object on the right, and 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.

The variants below all run against the customer object above. The only thing that changes from tab to tab is which extension you reach for.

const { name: customerName } = customer;

Rename. Reach for this when the outer scope already has a name binding that the destructure would shadow, or when the local context wants a more specific name than the field’s. The syntax reads backwards from the object-literal shorthand you might expect: left of the : is the field name on the source, and right of the : is the local binding being created. After this line, customerName is in scope and name is not. Readers often get this direction backwards, so if it surprised you, reread the sentence before moving on.

Since the rename is the form that trips first-time readers, 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.

Array destructuring: the two daily reaches

Section titled “Array destructuring: the two daily reaches”

Array destructuring is the companion form, and there is much less of it to learn. Object destructuring picks fields by name and ignores order; array destructuring binds by position. The shape looks like this:

const pair = ['admin', true];
const [role, isActive] = pair;
const [, isActive2] = pair;
const [first, ...rest] = ['a', 'b', 'c'];

The three destructuring lines show the three forms. The first binds the two positions of the array to local variables in order. The second leaves an empty slot before the comma to skip the first position and bind only the second, which is useful when you need just one slot out of a tuple. The third is the head-and-rest split: it binds the first item and collects every remaining item into a new array bound to rest.

There is one rule for when to reach for it at all: use array destructuring when the data is meaningfully ordered. Three cases fit that rule. A useState return value, 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. Don’t reach for it on a plain array of records where position carries no meaning; there you’re better off iterating and naming each item.

You’ll meet this form in two specific places later. In the chapter on the React render model, useState returns [value, setter] tuples that you destructure positionally. In the next chapter on containers, Object.entries yields [key, value] pairs that you destructure inside for...of.

Signature-level destructure: the canonical 2026 shape

Section titled “Signature-level destructure: the canonical 2026 shape”

This section covers the literal form that every Server Action, every React component, and every options-object helper in this course will write. The destructure happens at the parameter position: 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 shape the default every framework in the course expects:

  1. Names match the call site. The caller writes createInvoice({ customerId, amountCents }); the body reads customerId and amountCents. There’s no const customerId = options.customerId boilerplate and no mental remapping between the call and the implementation; both sides of the function share the same names.
  2. Adding a field never breaks existing callers. Field order is irrelevant because the destructure picks fields by name. A new optional field on the type adds a capability without touching any call site that doesn’t use it.
  3. Defaults live at the signature. The body never starts with const notes = options.notes ?? ''. The default sits at the field, visible in the signature where a reviewer looks for the contract.

Every options-object function in the rest of the course writes this shape. React component props in the chapter on components and composition are this shape, spelled in JSX. Server Action input in the chapter on the server / client boundary is this shape. The Drizzle query-builder helpers in the Postgres unit take their options this way too. Once the form is in your fingers, you’ll reach for it on every function past two parameters.

One last point to make explicit: signature destructure is the default for API-shaped functions. The alternative is to destructure on the first line of the body, with the parameter named options. That alternative is the right choice only when the original options reference needs to stay in scope, for example to log the full payload, forward it to an SDK, or reference it more than once before a destructure could cover every use. 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”

The sections above covered the form. This one covers the discipline for using it, which is also the structural fix for the wholesale-forward bug from the lesson’s first paragraph.

When forwarding data to a downstream call, destructure exactly the fields needed and rebuild the object literal that goes downstream. Never forward the original object wholesale.

Walk through the before and after below. The shape of the function stays nearly the same; the difference is in the data flowing out.

const sendInvoice = (input: SendInvoiceInput) => {
db.insert(invoicesTable).values(input);
email.send({ to: input.customerEmail, ...input });
};

A sendInvoice handler receives an invoice payload, { customerEmail, amountCents, internalNotes }, and forwards the whole input to two downstream callees. The database insert is fine: the full row genuinely belongs there. The email.send call is the leak, because the customer-facing email body now interpolates every field on input, including internalNotes (“flag for collections, third late payment”). Nothing at this call site mentions internalNotes, which is exactly why a reviewer skimming the function misses the bug. It gets worse over time. When someone upstream adds a new admin-only field to SendInvoiceInput, say a riskScore or a routingMemo from a future invoicing flow, that field lands in the customer’s inbox too, with zero code change here.

This is the same principle as the options-object pattern from the previous lesson, make the surface area visible at the call site, now applied to the outgoing side of a function. The options object made the inputs explicit; the rebuilt object literal makes the forwarded fields explicit. Both work for the same reason: a field added somewhere else can’t silently reach a new place, because data only flows where a developer has explicitly typed its name.

This is where the rest extension from earlier in the lesson does its main job: it omits a specific field by name and forwards everything else deliberately.

const { internalNotes, ...customerSafeFields } = invoice;
return customerSafeFields;

This is the canonical “strip a sensitive field” idiom. It’s the shape a serializeInvoice helper takes before sending a record to the client, the shape an email adapter takes before composing a customer-facing message, and the shape 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 you’re picking a few fields to forward, name them positively ({ customerEmail, amountCents }). When you’re omitting one specific field from an otherwise-safe shape, the rest pattern is the cleanest reach. The underlying principle is the same either way.

The Server Actions chapter later in the course applies this same discipline to the fields passed into Drizzle’s .values(…) call: only what was destructured by name reaches the table. You learn the form here, and the rest of the course puts it to work.

One mechanic remains to cover before the closing exercise. Destructuring can nest, meaning a single pattern can reach two or three levels into an object. The moment a nullable link sits in the middle of that chain, the destructure throws at runtime.

const { profile: { address: { city } } } = user;

That line throws if either user.profile or user.profile.address is nullish, because the destructure has to dereference each level to reach city, 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 was built for exactly this job, stepping through nullable links and short-circuiting safely, and it reads more cleanly at every level past the first.

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 genuinely guaranteed, as in a hardcoded config or the result of a schema-validated parse. Past one level of nesting, though, the reading cost climbs quickly, and the ?. form usually wins on both clarity and safety. Pick the form that matches the type’s nullability. This is the same discipline as the overuse-trap section in the previous lesson.

To close the lesson, sort six destructure snippets into three buckets that map to the three pieces of the model: syntactic validity (does the parser accept it?), semantic correctness (does the default fire when you’d 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.

Valid Parses fine, won't throw on the given input.
Syntactically wrong The parser rejects this; TypeScript flags it before runtime.
Throws at runtime Parses fine; throws a TypeError when a nullish link is dereferenced.
const { name, email } = customer;
// Given: user.profile = null
const { profile: { address } } = user;
const { name = 'Anon' } = { name: '' };
const { ...rest, id } = customer;
const { id: , name } = customer;
// Given: order.customer = undefined
const { customer: { id } = {} } = order;

Sorting these six checks your recall of the three pieces of the model: the form (what the parser accepts), the semantics (defaults fire only on undefined), and the runtime safety (whether the chain dereferences a nullish link). From here on, every options-object function you write opens with a destructure that obeys all three rules, whether it’s a Server Action, a React component, or a Drizzle helper.