Destructuring, rest properties and object shorthand

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. Of course, you can 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.

What even is Vanilla JS these days?

Originally it was non-jQuery, right? Or did it come before that? Anyway the term definitely got popular when people were eschewing jQuery in the quest for lighter pages at the expense of a few browser bugs.

Zero dependency libraries became a thing, which meant each library had their own tiny abstraction of DOM selection utilities and polyfills for Array methods. None of which could be extracted into shared dependencies and cached separately of course but, hey, they were 10x lighter and 20x faster than jQuery so what was there to worry about?

Then jQuery fell way off the radar due to a surge in browsers becoming evergreen, our eagerness to drop older, painful, browsers, and the proliferation of sites like youmightnotneedjquery.com. With jQuery out of the equation these days vanilla is much more likely to refer to the absence of frameworks like Ember, Angular, React or Backbone, of which only the latter requires jQuery.

In Paul Lewis’ recent article on the performance comparisons of frameworks he highlighted a vanillajs implementation of TodoMVC which was 16kb: significantly smaller than the other frameworks but certainly not tiny. Primarily it’s smaller as it is can be focused on this one specific purpose allowing for greater optimisation but making it somewhat throwaway after the life of the project. And, of course, it still has to reimplement a bunch of the same features that are present in other libs.

What makes this vanilla? Sure, it doesn’t have any dependencies but what makes up that 16kb?

It includes tiny abstractions for querySelectorAll and DOM events which you’d absolutely expect as developer conveniences. It include it’s own implementation of a micro templating library which focuses only on the todo template but still covers non-trivial html escaping.

It registers model.js, controller.js and view.js, it is todoMVC after all but it’s starting to look suspiciously like my-framework.js rather than vanillajs. In fact it’s really looking like a less-tested and less-jQuery snowflake version of backbone. This isn’t hating on the particular example on the todomvc page it just gets you wondering where the line is drawn between vanillajs and.. flavoured js?

Is it vanillajs if you don’t include a framework but you do include lots of tiny libs as dependencies? Is it vanillajs if it’s written in TypeScript? Is it wise to care about any of this? Is it a worthy goal?

Whilst your own implementation of these features can be smaller and more focused, certainly more performant, is chasing this title going to create a less buggy application? Will it be safer and more secure than a framework which has the benefit of a huge user base and collective intelligence? Are you going to have to reimplement features every time requirements change and could this lack of manoeuvrability end up causing costs to you and your user greater than the extra perf differences?

Anyone who I’ve worked with surely attest that I’m not a fan of debating terminology. It gets in the way of doing actual work and, truthfully, everyone else is better than me at it anyway. Vanillajs is a term that is gathering so much momentum though, and conflating so many ambiguous combinations that it either needs to be defined or descend into utter meaninglessness. And if it’s the latter we need to go back and update thousands of blog posts and slidedecks so maybe it’s best to just nip it in the bud now.