Learn JS Series (#21) - Destructuring Parameters: Named Arguments the JS Way
Learn JS Series (#21) - Destructuring Parameters: Named Arguments the JS Way
What will I learn
- You will learn array and object destructuring, and how to pull values out cleanly;
- how to destructure function parameters to get readable "named arguments";
- how to combine destructuring with default values for optional settings;
- how to rename destructured properties and provide fallbacks;
- why an options object beats a long list of positional parameters.
Requirements
- A working modern computer running macOS, Windows or Ubuntu;
- An installed Node.js (20+) distribution, or just a modern browser console;
- Episodes 1-20 read, especially objects, arrays, and default/rest/spread.
Difficulty
- Beginner
Curriculum (of the Learn JS Series):
- Learn JS Series (#1) - What Is JavaScript, Why It Runs Everywhere, and How to Run It
- Learn JS Series (#2) - Variables and Bindings
- Learn JS Series (#3) - The Primitive Types: number, string, boolean, null, undefined, symbol, bigint
- Learn JS Series (#4) - Operators and Expressions: Arithmetic, Comparison, Logical, and Short-Circuiting
- Learn JS Series (#5) - Strings: Template Literals, Unicode, and the Methods You Actually Use
- Learn JS Series (#6) - Numbers: IEEE 754, Why 0.1 + 0.2 Is Not 0.3, and How to Cope
- Learn JS Series (#7) - Control Flow: if/else, switch, and the Ternary Expression
- Learn JS Series (#8) - Loops: for, while, for...of, for...in, and When to Use Which
- Learn JS Series (#9) - Functions: Declarations, Parameters, Return Values, and Hoisting
- Learn JS Series (#10) - Scope and the Temporal Dead Zone: How JavaScript Finds Your Variables
- Learn JS Series (#11) - Arrays: The Workhorse Data Structure and Its Core Methods
- Learn JS Series (#12) - Objects: Key-Value Data, Dot vs Bracket Access, and Nesting
- Learn JS Series (#13) - Truthiness, Equality, and Coercion: == vs === Done Properly
- Learn JS Series (#14) - Mini Project: A Command-Line Tip Calculator
- Learn JS Series (#15) - First-Class Functions: Passing, Returning, and Storing Functions
- Learn JS Series (#16) - Arrow Functions vs function: Syntax, this, and When Each Wins
- Learn JS Series (#17) - Closures: The Single Most Important Idea in JavaScript
- Learn JS Series (#18) - Higher-Order Functions: Functions That Take or Return Functions
- Learn JS Series (#19) - Callbacks and the Callback Pattern (Before We Reach Promises)
- Learn JS Series (#20) - Default, Rest, and Spread: Flexible Function Signatures
- Learn JS Series (#21) - Destructuring Parameters: Named Arguments the JS Way (this post)
Learn JS Series (#21) - Destructuring Parameters: Named Arguments the JS Way
Solutions to Episode 20 Exercises
Exercise 1 - an average of any count:
function average(...nums) {
if (nums.length === 0) return 0;
return nums.reduce((a, b) => a + b, 0) / nums.length;
}
console.log(average(2, 4, 6)); // 4
console.log(average()); // 0
The insight: the rest parameter gives a real array, so length and reduce are right there, and we guard the empty case to avoid dividing by zero (which would hand you back NaN, not an error).
Exercise 2 - merging objects:
function merge(...objects) {
return objects.reduce((acc, obj) => ({ ...acc, ...obj }), {});
}
console.log(merge({ a: 1 }, { b: 2 }, { a: 9 })); // { a: 9, b: 2 }
The insight: spreading each object in turn, later keys override earlier ones, so a ends up 9. Notice the parentheses around ({ ...acc, ...obj }) -- without them the arrow body would be read as a block, not an object literal.
Exercise 3 - min and max via spread:
const nums = [3, 1, 4, 1, 5, 9];
console.log(Math.min(...nums)); // 1
console.log(Math.max(...nums)); // 9
The insight: ... spreads the array into separate arguments for Math.min/Math.max; the same ... in a parameter list would instead gather arguments into an array. Same three dots, opposite direction -- which is exactly the symmetry we hammered on last episode.
Now let's put destructuring to work, because it is the tool that makes function calls dramatically more readable, and it pairs beautifully with the defaults you just learned.
Destructuring basics
Destructuring is a syntax for unpacking values out of arrays and objects into individual variables, in one clean line. We already saw glimpses of it (episodes 11 and 12 both leaned on it), but here it is properly, front and center. Array destructuring pulls values out by position:
const point = [10, 20, 30];
const [x, y, z] = point;
console.log(x, y, z); // 10 20 30
const [first, , third] = point; // skip the middle with an empty slot
console.log(first, third); // 10 30
That empty slot in [first, , third] is not a typo -- it is a deliberate "skip this position" hole. Handy when a function or API hands you a tuple and you only care about some of its elements.
Object destructuring pulls properties out by name (order does not matter here, the names do):
const user = { name: "scipio", level: 7, city: "amsterdam" };
const { name, level } = user;
console.log(name, level); // "scipio" 7
The variable names must match the property names, because for objects it is the key that identifies what you want, not a position. This is already handy on its own, but its real power shows up the moment you move it into a function's parameter list. That is where destructuring stops being a convenience and starts being a genuine design tool.
Destructuring array parameters
You can destructure directly in a function's parameter list. For a function that receives an array (like a coordinate pair), this names the parts immediately, right there in the signature:
function distanceFromOrigin([x, y]) {
return Math.sqrt(x * x + y * y);
}
console.log(distanceFromOrigin([3, 4])); // 5
The parameter [x, y] says, in effect, "I expect an array, and I want its first two elements bound to x and y". No arr[0], no arr[1], no mental bookkeeping. This pairs especially well with methods that hand you pairs, like iterating over Object.entries, where each element is a [key, value] array:
const scores = { math: 90, art: 78 };
Object.entries(scores).forEach(([subject, score]) => {
console.log(`${subject}: ${score}`);
});
// math: 90
// art: 78
Each [subject, score] destructures the pair right in the callback parameter -- no pair[0]/pair[1] noise cluttering the body. Having said that, array destructuring in parameters is a smaller win than the object version, simply because positional arrays carry the same "which slot means what?" burden that positional arguments do. So let's get to the pattern that genuinely changes how you write functions.
Destructuring object parameters: named arguments
Here is the pattern that transforms real code. When a function needs several inputs, especially optional ones, passing them positionally is fragile: the caller must remember the exact order, and skipping a middle one is awkward (you have to pass undefined as a placeholder, as we saw last episode). The fix is to accept a single options object and destructure it right in the parameter list. Now every argument is named at the call site:
function createUser({ name, age, role }) {
return `${name} (${age}), role: ${role}`;
}
console.log(createUser({ name: "scipio", role: "admin", age: 30 }));
// "scipio (30), role: admin"
Look carefully at that call: { name: "scipio", role: "admin", age: 30 }. The order is irrelevant -- I deliberately wrote role before age to prove the point -- and each value is labelled by the key sitting next to it. Compare that to the positional alternative, createUser("scipio", 30, "admin"), where a reader has no earthly idea what 30 or "admin" are supposed to mean without going and reading the function definition. Multiply that across a codebase and the difference is enormous.
The named arguments style buys you three concrete things. First, calls are self-documenting -- you read the call and you know what each value is for. Second, order stops mattering, so nobody ever swaps two same-typed arguments by accident (the classic createRect(width, height) versus createRect(height, width) bug simply cannot happen). Third, and this is the quiet hero, you can add a new option later without breaking a single existing call, because old calls just do not mention the new key. Positional signatures do not give you that for free -- add a fourth positional parameter and every call site has to reckon with it.
Defaults with destructured parameters
Destructuring combines with the defaults from episode 20 to make optional settings genuinely clean. You give each destructured property its own fallback, so callers only pass what they actually want to override:
function connect({ host, port = 8080, secure = false } = {}) {
return `${secure ? "https" : "http"}://${host}:${port}`;
}
console.log(connect({ host: "example.com" })); // "http://example.com:8080"
console.log(connect({ host: "example.com", secure: true }));// "https://example.com:8080"
There are actually two layers of defaulting at work here, and it is worth pulling them apart because newcomers often see only one. The inner defaults -- port = 8080 and secure = false -- kick in per property when that specific key is missing or undefined in the object you passed. The outer = {} at the very end is a different thing entirely: it defaults the whole parameter to an empty object when you call connect() with no argument at all.
That outer = {} is not optional politeness -- it is load-bearing. Without it, calling connect() throws a TypeError, because JavaScript would try to destructure host, port, and secure out of undefined, and you cannot read properties off undefined. With the = {} guard, a bare connect() first substitutes an empty object, then the inner defaults fill in port and secure, and only host comes back undefined. Remember this one -- forgetting the = {} on a destructured options parameter is one of the most common little crashes you will write in your first month, and now you will recognise it instantly:
function badConnect({ host, port = 8080 }) { // no = {} guard
return `${host}:${port}`;
}
// badConnect(); // TypeError: Cannot destructure property 'host' of 'undefined'
console.log(badConnect({ host: "example.com" })); // "example.com:8080" (fine WITH an arg)
So the rule of thumb is simple and mechanical: any time you destructure an object in a parameter list and you want the function to be callable with no arguments, end the pattern with = {}. It costs four characters and saves a crash.
Renaming and nested destructuring
Two more tricks you will reach for constantly. First, you can rename a property as you destructure it, using the originalName: newName form. This is invaluable when the source property name is cryptic, or when it would clash with a variable you already have in scope:
const response = { d: "2026-08-13", v: 42 };
const { d: date, v: value } = response;
console.log(date, value); // "2026-08-13" 42
Read d: date as "take the property d, and bind it to a local variable called date". Nota bene: this trips people up because it looks like the object-literal syntax for the opposite direction. In an object literal { date: d } means "make a key date from variable d"; in a destructuring pattern { d: date } means "read key d into variable date". Same colon, mirror-image meaning depending on which side of the = you are on -- keep that straight and renaming becomes second nature.
Second, you can destructure nested structures in one go, reaching down into inner objects:
const order = {
id: 1,
customer: { name: "scipio", address: { city: "amsterdam" } },
};
const { customer: { name, address: { city } } } = order;
console.log(name, city); // "scipio" "amsterdam"
Notice something subtle in that pattern: customer: { ... } does NOT create a customer variable. The customer: part is purely a path -- it tells JavaScript where to dig, and only the innermost names (name, city) actually become variables. If you also wanted the customer object itself, you would have to list it separately. And a word of caution born from experience: nested destructuring gets hard to read fast if you go too deep. One or two levels is crisp and clear; five levels is a puzzle nobody wants to decode at 2am. Use it for pulling a couple of nested values, not as a party trick.
Rest in destructuring
The rest syntax from episode 20 works inside destructuring too, gathering "everything else" into a fresh array or object. On the array side it splits off a head and a tail; on the object side it grabs a few named properties and sweeps the remainder into a new object:
const [head, ...tail] = [1, 2, 3, 4];
console.log(head, tail); // 1 [2, 3, 4]
const { id, ...rest } = { id: 1, name: "scipio", level: 7 };
console.log(id); // 1
console.log(rest); // { name: 'scipio', level: 7 }
The object version is especially useful for the "remove one property, immutably" move: destructure out the key you want gone, and rest is a brand-new object that has everything except that key -- without mutating the original at all. This is a pattern you will see everywhere in modern JavaScript (React state updates lean on it heavily, for one), so it is worth burning into memory now:
function withoutPassword(user) {
const { password, ...safe } = user;
return safe; // a new object with everything except password
}
console.log(withoutPassword({ name: "scipio", password: "hunter2", level: 7 }));
// { name: 'scipio', level: 7 }
The original user is untouched, safe is a fresh object, and the sensitive field never leaves the function. Clean, declarative, and no delete in sight -- which matters, because delete mutates the original and (as we will see much later in the series) can hurt performance by deoptimising the object's shape.
How other languages handle this
A lot of you came here from the Learn Python Series, and a few from Rust and Go, so a quick look sideways helps place destructuring in the bigger picture. The reassuring news: unpacking-into-variables and named-arguments are near-universal ideas -- only the spelling and the philosophy shift.
Python is the closest cousin, and if you know it the mapping is almost one-to-one. Python has sequence unpacking (a, b, c = triple), a *rest catch-all, and -- crucially -- real keyword arguments baked into the language, which is exactly what JavaScript is simulating with its options-object trick:
point = [10, 20, 30]
first, *rest = point # first = 10, rest = [20, 30]
print(first, rest)
def connect(host, port=8080, secure=False): # genuine keyword args
scheme = "https" if secure else "http"
return f"{scheme}://{host}:{port}"
print(connect("example.com", secure=True)) # named at the call site
Here is the interesting contrast: Python gives you named arguments for free at the language level (connect(host="example.com", secure=True)), so it never needs the options-object pattern. JavaScript has no built-in keyword arguments, so the community reached for "pass one object and destructure it" -- and it works so well that it became the idiomatic way to write any function with more than two or three options. Same destination, different road.
Rust unpacks through its pattern-matching system, which is far more powerful (it drives match, if let, and let bindings), and it destructures structs by field name in a way that will feel oddly familiar:
struct Config { host: String, port: u16 }
fn describe(Config { host, port }: Config) -> String {
format!("{}:{}", host, port)
}
fn main() {
let c = Config { host: "example.com".to_string(), port: 8080 };
println!("{}", describe(c)); // example.com:8080
}
That Config { host, port } in the parameter position is destructuring a struct right in the function signature -- the same shape as JavaScript's function f({ host, port }), just with the type named up front. Rust does not have optional/default parameters at all, though; the idiomatic Rust answer is the builder pattern or an explicit Option<T>, reflecting its "no hidden behaviour" values.
Go, true to form, keeps it minimal. It has multiple return values and can unpack them into several variables, but it has no object-destructuring in parameters and no default arguments whatsoever. The Go answer to "many optional settings" is, once again, to pass a config struct explicitly:
package main
import "fmt"
type Config struct {
Host string
Port int
Secure bool
}
func connect(c Config) string {
scheme := "http"
if c.Secure {
scheme = "https"
}
return fmt.Sprintf("%s://%s:%d", scheme, c.Host, c.Port)
}
func main() {
fmt.Println(connect(Config{Host: "example.com", Port: 8080}))
}
Notice that a Go Config{Host: "example.com"} literal reads almost exactly like a JavaScript options object -- named fields, order-independent -- which is precisely why the pattern feels natural coming from either direction. Four languages, one recurring lesson: once a function grows past a couple of arguments, humans want names, not positions. JavaScript just gets there by destructuring an object rather than baking keyword arguments into the grammar.
Try it yourself
Three exercises, increasing in difficulty. Type them out, run them, and predict the output before you check -- that gap between your guess and the actual result is where the real learning lives. Full solutions open the next episode.
- Given
const rgb = [255, 128, 0], destructure it intored,green,blueand print each. Then swap two variables using array destructuring ([a, b] = [b, a]) and show it works without a temporary variable. - Write a function
drawBox({ width, height, char = "*" })that returns a string describing a box, using the namedcharwith a default. Call it once with all three properties and once relying on thechardefault. - Write a function
describe({ name, ...details })that separates thenamefrom all other properties, returning a string like"scipio has extras: {...}". Then explain, in one sentence, why adding= {}to that destructured parameter matters when the function is called with no arguments.
So what did we actually cover?
- Destructuring unpacks arrays by position (
[x, y]) and objects by name ({ name, level }) into variables in a single line, and an empty slot lets you skip array positions you do not want. - You can destructure directly in a function's parameter list, naming array parts or object properties immediately -- no
arr[0]orobj.propbookkeeping in the body. - Accepting one options object and destructuring it gives you named arguments: order-independent, self-documenting, and safe to extend with new options later.
- Combine it with per-property defaults AND a
= {}fallback for the whole object, so a no-argument call fills in sensibly instead of throwing aTypeError. - You can rename (
d: date), reach into nested structures, and use rest (...tail,...rest) to gather the remainder -- the neat, non-mutating "remove a property" trick. - Other languages solve the same human need for names-over-positions: Python and Rust with real language features, Go with explicit config structs -- JavaScript gets there by destructuring an object.
Next episode we finally confront the this keyword head-on: five rules that, once you know them, explain every single case of what this refers to -- and clear up one of JavaScript's most notorious sources of confusion for good.