Destructuring, rest properties and object shorthand
How you can use these features to write more maintainable code
Posted on | Comments
Destructuring and rest/spread parameters for Arrays is part of the es6 specification. Support for their use with Objects is, at the time of writing, a stage 2 proposal for future inclusion. You can also use it today via a transpiler like Babel.
Object Shorthand is already part of the es6 specification and with a combination of these three features you can start to use some patterns which can lead to more reliable, less error prone code.
First, let's dig in to how destructuring objects looks. We'll take a simple config object and use destructuring to extract some values.
let config = {
env: 'production',
user: { name: 'ian' }
};
let { env } = config;
console.log(env); // 'production'
console.log(config); // { env: 'production', user: { name: 'ian' } }
The equivalent in es5 would be:
var config = {
env: 'production',
user: { name: 'ian' }
};
var env = config.env;
Note that the original config object never mutates. The benefits of immutability are well documented and, whilst this isn't strictly immutable, starting to write in a way which maintains the original values allows you to reason about the code more easily.
We can also go one step further and destructure two levels deep:
let { env, user: { name } } = config;
console.log(name); // 'ian'
console.log(user); // err: user is not defined
Now let's introduce rest properties:
let { env, ...newConfig } = config;
console.log(env); // 'production'
console.log(newConfig); // { user: { name: 'ian' } }
console.log(config); // { env: 'production', user: { name: 'ian' } }
Using those three dots creates a new object which represents everything that remains in the config after you have taken out the named variables.
Note that this will still create an empty object so you can rely on object methods working without knowing what the data might be:
let { env, user: { name }, ...newConfig } = config;
console.log(newConfig); // {}
These are solid primitives which can be built up into useful patterns. One way in which they can help immediately is by removing connascence. Connascence relates to the relationship between two components where a change in one would require a change in the other to maintain functionality. A way in which this has often transpired in my code is with argument ordering in functions, particularly functions with high arity.
Let's take a typical analytics function:
function trackAnalytics(label, category, dimension, username, email) {
window.track(label, category, dimension, username, email);
}
trackAnalytics('login', 'user', 'app1', 'ian', 'test@test.com');
Assuming that these functions are in different files, aside from the email address it's pretty hard to tell from the call side what each parameter relates to. It also breaks if we get the order wrong.
trackAnalytics('user', 'login', 'app1', 'ian', 'test@test.com'); // Tracking is broken
A way in which this is typically resolved is by switching to passing a single object as a parameter and naming the values within it. Now you no longer need to care about understanding their role or the order in which they're included.
trackAnalytics({
label: 'login',
category: 'user',
dimension: 'app1',
username: 'ian',
email: 'test@test.com'
});
Which means you can satisfy your OCD by ordering them alphabetically or in pyramid style.
trackAnalytics({
label: 'login',
username: 'ian',
category: 'user',
dimension: 'app1',
email: 'test@test.com'
});
That's better. So passing this object removes the connascence and improves the call side but it has suddenly got worse on the function side:
function trackAnalytics(data) {
window.track(data.label, data.category, data.dimension, data.username, data.email);
}
We no longer know what's inside data
and in a function that was more complex we'd probably have to resort to documenting the function arguments in a jsdoc fashion. This can be pretty useful anyway but we can remove the need for it to some extent by using destructuring (note the braces within the arguments list).
function trackAnalytics({ label, category, dimension, username, email }) {
window.track(label, category, dimension, username, email);
}
trackAnalytics({
label: 'login',
category: 'user',
dimension: 'app1',
username: 'ian',
email: 'test@test.com'
});
Now the order of the arguments no longer matters and we understand what the values represent on both sides of the function contract.
Often we may already have these values wrapped up in variables before we send them to the function. If that is the case we can take advantage of another feature: object shorthand. Object shorthand allows you to replace the key value pair by a single key. For example:
// Assume these would already exist
let label = 'login';
let category = 'user';
let dimension = 'app1';
let username = 'ian';
let email = 'test@test.com';
trackAnalytics({
label,
category,
dimension,
username,
email
});
Which means we can go back to a more simple looking call, without any concern about order.
function trackAnalytics({ label, category, dimension, username, email }) {
window.track(label, category, dimension, username, email);
}
trackAnalytics({ category, label, dimension, username, email }); // ✔
trackAnalytics({ category, username, email, label, dimension }); // ✔
At this point we've already made the code much more robust and resistant to errors whilst keeping the code very simple and readable.
Another trick we have with these three features is to destructure within arguments themselves. In our example, we have the username and email in our original config object so let's take them from there.
let config = {
env: 'production',
user: { name: 'ian' }
};
function trackAnalytics(label, category, dimension, { env, user }) {
if (env !== 'production') { return };
window.track(label, category, dimension, user.name, user.email);
}
trackAnalytics('login', 'user', 'app1', config);
We can even take it one step further and remove the user object:
function trackAnalytics(label, category, dimension, { env, user: { name, email } }) {
if (env !== 'production') { return };
window.track(label, category, dimension, name, email);
}
Maybe that's going a bit far... These are just tools for you to use however you see fit though.
Hopefully this serves as an example of how you can use these new features to write safer, simpler code. There's a lot of sytactic sugar in the new JS features but coupled together they achieve things which would be significantly harder, or at least more verbose, to write in ES5.