Destructuring - Exercise 3
When Promise.all resolves, it returns an array of results in the same order as the input promises. You can destructure this array directly to get each result in a named variable.
const fetchUser = () => Promise.resolve({ name: 'Alice' });
const fetchPosts = () => Promise.resolve([{ id: 1 }, { id: 2 }]);
const fetchSettings = () => Promise.resolve({ theme: 'dark' });
// Destructure Promise.all results
const [user, posts, settings] = await Promise.all([
fetchUser(),
fetchPosts(),
fetchSettings()
]);
console.log(user); // { name: 'Alice' }
console.log(posts); // [{ id: 1 }, { id: 2 }]
console.log(settings); // { theme: 'dark' }
// You can also destructure nested data
const [{ name }, [firstPost], { theme }] = await Promise.all([
fetchUser(),
fetchPosts(),
fetchSettings()
]);
console.log(name); // 'Alice'
console.log(firstPost); // { id: 1 }
console.log(theme); // 'dark'
You can destructure error objects in catch blocks to extract specific properties like message, name, or custom error fields. This makes error handling cleaner and more readable.
// Destructure standard error properties
try {
throw new Error('Something went wrong');
} catch ({ message, name }) {
console.log(name); // 'Error'
console.log(message); // 'Something went wrong'
}
// Destructure custom error properties
class ApiError extends Error {
constructor(message, statusCode, details) {
super(message);
this.statusCode = statusCode;
this.details = details;
}
}
try {
throw new ApiError('Not found', 404, { resource: 'user' });
} catch ({ message, statusCode, details }) {
console.log(message); // 'Not found'
console.log(statusCode); // 404
console.log(details); // { resource: 'user' }
}
// With default values for optional properties
try {
throw new Error('Failed');
} catch ({ message, statusCode = 500 }) {
console.log(message); // 'Failed'
console.log(statusCode); // 500 (default)
}
Object.entries returns an array of key value pairs. You can destructure each pair in a loop or callback to access both the key and value directly without array indexing.
const prices = {
apple: 1.5,
banana: 0.75,
orange: 2.0
};
// Destructure in for...of loop
for (const [fruit, price] of Object.entries(prices)) {
console.log(`${fruit}: $${price}`);
}
// apple: $1.5
// banana: $0.75
// orange: $2.0
// Destructure in forEach
Object.entries(prices).forEach(([fruit, price]) => {
console.log(`${fruit} costs ${price}`);
});
// Destructure in map to transform data
const priceList = Object.entries(prices).map(([name, cost]) => ({
name,
cost,
formatted: `$${cost.toFixed(2)}`
}));
console.log(priceList);
// [{ name: 'apple', cost: 1.5, formatted: '$1.50' }, ...]
// Filter using destructured values
const expensive = Object.entries(prices)
.filter(([, price]) => price > 1)
.map(([fruit]) => fruit);
console.log(expensive); // ['apple', 'orange']
Object.keys and Object.values return arrays that can be destructured to extract specific elements. This is useful when you need only certain keys or values from an object.
const config = {
host: 'localhost',
port: 3000,
protocol: 'https',
timeout: 5000
};
// Destructure first few keys
const [firstKey, secondKey] = Object.keys(config);
console.log(firstKey, secondKey); // 'host' 'port'
// Destructure first few values
const [host, port] = Object.values(config);
console.log(host, port); // 'localhost' 3000
// Skip elements with commas
const [, , protocol] = Object.values(config);
console.log(protocol); // 'https'
// Use rest to get remaining items
const [primary, ...otherKeys] = Object.keys(config);
console.log(primary); // 'host'
console.log(otherKeys); // ['port', 'protocol', 'timeout']
// Combine with spread for object manipulation
const keys = Object.keys(config);
const values = Object.values(config);
const [mainKey, ...restKeys] = keys;
const [mainValue, ...restValues] = values;
console.log(mainKey, mainValue); // 'host' 'localhost'
Class instances can be destructured just like regular objects. You can extract any public properties from the instance. However, methods are not included when destructuring.
class User {
constructor(name, email, role) {
this.name = name;
this.email = email;
this.role = role;
this.createdAt = new Date();
}
greet() {
return `Hello, ${this.name}!`;
}
}
const user = new User('Alice', 'alice@example.com', 'admin');
// Destructure instance properties
const { name, email, role } = user;
console.log(name); // 'Alice'
console.log(email); // 'alice@example.com'
console.log(role); // 'admin'
// Destructure with renaming
const { name: userName, role: userRole } = user;
console.log(userName); // 'Alice'
console.log(userRole); // 'admin'
// Destructure in function parameters
function displayUser({ name, email }) {
console.log(`${name} (${email})`);
}
displayUser(user); // 'Alice (alice@example.com)'
// Note: Methods are not destructured as standalone functions
const { greet } = user;
// greet() would lose 'this' context without binding
In React, destructuring props makes components cleaner and more readable. You can destructure directly in the function parameter or inside the component body with default values.
// Destructure props in function parameter
function UserCard({ name, email, avatar, isOnline = false }) {
return (
<div className="user-card">
<img src={avatar} alt={name} />
<h2>{name}</h2>
<p>{email}</p>
{isOnline && <span>Online</span>}
</div>
);
}
// Destructure with rest for passing remaining props
function Button({ children, variant = 'primary', ...rest }) {
return (
<button className={`btn btn-${variant}`} {...rest}>
{children}
</button>
);
}
// Usage: <Button onClick={handleClick} disabled={true}>Click</Button>
// Nested destructuring for complex props
function ProductItem({ product: { name, price, category } }) {
return (
<div>
<h3>{name}</h3>
<p>${price} - {category}</p>
</div>
);
}
// Destructure children separately
function Layout({ children, sidebar, header }) {
return (
<div>
<header>{header}</header>
<aside>{sidebar}</aside>
<main>{children}</main>
</div>
);
}
When destructuring potentially undefined or null values, you can combine optional chaining with the nullish coalescing operator to provide safe fallbacks and avoid runtime errors.
const response = {
data: {
user: {
profile: {
name: 'Alice',
address: null
}
}
}
};
// Safe destructuring with optional chaining and fallback
const { city = 'Unknown' } = response.data?.user?.profile?.address ?? {};
console.log(city); // 'Unknown'
// Destructure existing nested data safely
const { name } = response.data?.user?.profile ?? {};
console.log(name); // 'Alice'
// Handle potentially missing parent objects
const maybeNull = null;
const { value = 'default' } = maybeNull ?? {};
console.log(value); // 'default'
// Combine with array destructuring
const apiResponse = { items: null };
const [firstItem = 'none'] = apiResponse.items ?? [];
console.log(firstItem); // 'none'
// Safe nested destructuring pattern
function processUser(data) {
const {
name = 'Guest',
settings: { theme = 'light', notifications = true } = {}
} = data ?? {};
return { name, theme, notifications };
}
console.log(processUser(null));
// { name: 'Guest', theme: 'light', notifications: true }
console.log(processUser({ name: 'Bob' }));
// { name: 'Bob', theme: 'light', notifications: true }
API responses often contain nested data structures. Destructuring helps you extract exactly the data you need while ignoring metadata like status codes or pagination info.
// Typical API response structure
const apiResponse = {
status: 200,
data: {
users: [
{ id: 1, name: 'Alice', email: 'alice@example.com' },
{ id: 2, name: 'Bob', email: 'bob@example.com' }
],
pagination: { page: 1, total: 50, perPage: 10 }
},
meta: { requestId: 'abc123' }
};
// Extract nested data directly
const { data: { users, pagination: { total } } } = apiResponse;
console.log(users); // Array of user objects
console.log(total); // 50
// Extract with fetch response
async function getUsers() {
const response = await fetch('/api/users');
const { data: { users } } = await response.json();
return users;
}
// Destructure first item from array in response
const { data: { users: [firstUser] } } = apiResponse;
console.log(firstUser); // { id: 1, name: 'Alice', ... }
// Extract multiple levels with renaming
const {
status: httpStatus,
data: { users: userList },
meta: { requestId: reqId }
} = apiResponse;
console.log(httpStatus); // 200
console.log(userList); // Array of users
console.log(reqId); // 'abc123'
// Destructure in async function parameters
async function processResponse({ data: { users }, meta }) {
console.log(`Processing ${users.length} users`);
console.log(`Request ID: ${meta.requestId}`);
}
Combining spread and destructuring is a powerful pattern for immutable object updates. You can extract properties, modify them, and create new objects without mutating the original.
const user = {
id: 1,
name: 'Alice',
email: 'alice@example.com',
role: 'user',
active: true
};
// Update specific properties immutably
const updatedUser = { ...user, role: 'admin' };
console.log(updatedUser.role); // 'admin'
console.log(user.role); // 'user' (unchanged)
// Remove a property using rest destructuring
const { active, ...userWithoutActive } = user;
console.log(userWithoutActive); // { id, name, email, role }
console.log(active); // true (extracted)
// Remove and update in one operation
const { email, ...rest } = user;
const newUser = { ...rest, email: 'new@example.com', verified: true };
// Update nested objects immutably
const state = {
user: { name: 'Alice', settings: { theme: 'dark' } },
posts: []
};
const newState = {
...state,
user: {
...state.user,
settings: { ...state.user.settings, theme: 'light' }
}
};
// Rename while spreading
const { name: oldName, ...otherProps } = user;
const renamed = { ...otherProps, fullName: oldName };
// Conditional property addition
const { role: userRole, ...baseUser } = user;
const adminUser = {
...baseUser,
...(userRole === 'admin' && { permissions: ['read', 'write', 'delete'] })
};
Destructuring configuration objects in function parameters allows for flexible APIs with sensible defaults. Users can pass only the options they want to customize.
// Function with configuration object parameter
function createServer({
port = 3000,
host = 'localhost',
protocol = 'http',
timeout = 5000,
cors = true,
logging = { level: 'info', format: 'json' }
} = {}) {
console.log(`Starting ${protocol}://${host}:${port}`);
console.log(`Timeout: ${timeout}ms, CORS: ${cors}`);
console.log(`Logging: ${logging.level} (${logging.format})`);
}
// Call with partial config
createServer({ port: 8080 });
// Uses port 8080, defaults for everything else
// Call with no config (uses all defaults)
createServer();
// Nested configuration with defaults
function initDatabase({
connection: {
host = 'localhost',
port = 5432,
database = 'app'
} = {},
pool: {
min = 2,
max = 10
} = {},
debug = false
} = {}) {
return { host, port, database, poolMin: min, poolMax: max, debug };
}
console.log(initDatabase({ connection: { port: 5433 } }));
// { host: 'localhost', port: 5433, database: 'app', poolMin: 2, poolMax: 10, debug: false }
// Validate and extract config
function validateConfig({ apiKey, endpoint, version = 'v1' }) {
if (!apiKey) throw new Error('API key is required');
if (!endpoint) throw new Error('Endpoint is required');
return { apiKey, endpoint, version };
}
Iterators and generators produce iterable sequences that can be destructured like arrays. You can extract values from Map, Set, or custom iterators using array destructuring syntax.
// Destructure Map entries
const userMap = new Map([
['alice', { age: 25 }],
['bob', { age: 30 }]
]);
for (const [name, data] of userMap) {
console.log(`${name}: ${data.age}`);
}
// Destructure first entries from Map
const [[firstKey, firstValue]] = userMap;
console.log(firstKey, firstValue); // 'alice' { age: 25 }
// Destructure Set values
const numbers = new Set([1, 2, 3, 4, 5]);
const [first, second, ...rest] = numbers;
console.log(first, second); // 1 2
console.log(rest); // [3, 4, 5]
// Destructure generator results
function* idGenerator() {
let id = 1;
while (true) yield id++;
}
const gen = idGenerator();
const [id1, id2, id3] = gen;
console.log(id1, id2, id3); // 1 2 3
// Destructure with custom iterator
const range = {
start: 1,
end: 5,
[Symbol.iterator]() {
let current = this.start;
const end = this.end;
return {
next() {
if (current <= end) {
return { value: current++, done: false };
}
return { done: true };
}
};
}
};
const [a, b, c] = range;
console.log(a, b, c); // 1 2 3
Common destructuring mistakes include destructuring null or undefined values, forgetting default values for nested objects, and losing the this context when destructuring methods.
// MISTAKE 1: Destructuring null or undefined
const data = null;
// const { name } = data; // TypeError: Cannot destructure property 'name' of null
// FIX: Use fallback
const { name } = data ?? {};
console.log(name); // undefined (no error)
// MISTAKE 2: Missing default for nested object
function greet({ user: { name } }) {} // Fails if user is undefined
// greet({}); // TypeError
// FIX: Add default for nested object
function greetFixed({ user: { name } = {} } = {}) {
console.log(name ?? 'Guest');
}
greetFixed(); // 'Guest'
// MISTAKE 3: Losing this context with methods
const obj = {
value: 42,
getValue() { return this.value; }
};
const { getValue } = obj;
// console.log(getValue()); // undefined (lost this)
// FIX: Bind the method or use arrow function
const boundGetValue = obj.getValue.bind(obj);
console.log(boundGetValue()); // 42
// MISTAKE 4: Confusing default value with fallback
const { count = 0 } = { count: null };
console.log(count); // null (default only applies to undefined)
// FIX: Use nullish coalescing after destructuring
const { count: rawCount } = { count: null };
const safeCount = rawCount ?? 0;
console.log(safeCount); // 0
// MISTAKE 5: Shadowing variables in nested destructuring
const user = 'outer';
// const { user: { name } } = { user: { name: 'Alice' } };
// console.log(user); // Still 'outer', not the nested object
// FIX: Use different variable name
const { user: userData } = { user: { name: 'Alice' } };
console.log(userData.name); // 'Alice'