Never use the Dot operator in JavaScript, for Data

Esbjörn Blomquist
6 min readAug 28, 2021

--

Photo by Divazus Fabric Store on Unsplash

The Dot operator is dangerous because if, at run-time, the value of the identifier before it is not what was intended, the program will likely throw an error and probably crash.

Firstly, there are of course cases where the dot operator is perfectly fine. Knowing where, and why, is what this article is about. But mostly it is about destructuring which I believe is the undervalued & underused solution.

Destructuring is undervalued & highly underused.

Secondly, let’s from the start acknowledge that with ES2019 two new operators were introduced to JavaScript:

  • Optional chaining: ?.
  • Nullish coalescing: ??

These make it possible to write user?.name ?? 'Noname' and not crash for non-existent user, and have a fallback for if any one of user or name does not exist.

These are fine, but not as great as destructuring.

But hey…

Let’s start from the beginning

Photo by Mikaela Stenström on Unsplash

Perhaps you’re playing with some home automation script, and you type out a helper function for some data structure, like below. You know that celsius is not always there so you throw in a fallback.

const getTemperature = (atticData) => atticData.celsius || 15;

This function will have two serious problems:

  • What if the object you give the function is uninitialized at some point, undefined? Then it will crash.
  • What if the Celsius temperature is 0? The function will return 15!

With the two new operators mentioned in the beginning, you fix the function:

const getTemperature = (atticData) => atticData?.celsius ?? 15;

The two problems are solved. This is great.

Should we use optional chaining operator everywhere now?

No. For two reasons:

  • It would just be in the way of readability for any known static structure, like an API. E.g. JSON.parse, window.localStorage.getItem or identifiers inside imported libraries.
  • Destructuring might be a better alternative in most other cases, where we handle any data.

Continuing with our example function above — it would, written with destructuring, look like this:

const getTemperature = ({ celsius = 15 } = {}) => celsius;

It is slightly cleaner, and it actually got more general in that it doesn’t necessarily lie about what it operates on. We avoid naming that. Any potential object could be given to this function, and it would attempt to safely extract a celsius.

Another way to think about it is that inside our function we never need to reference the data object, only stuff inside it. So we dig that out as early as possible. The function body can also not do anything with anything else than celsius, it could not for instance get the crazy idea to mutate the data object — it is not available.

Destructure as early as possible.

(The destructuring version is not strictly the same. See section about null last in the article.)

Changing habits

Whenever you type a dot, stop and think about if you could already have destructured out the value you’re after.

It often makes for shorter code with higher control. Mainly due to the ability to set defaults, but also because we can control what is available in the scope and how it’s available. We also have the possibility of destructuring in multiple levels, “nested destructuring”, and then set safe defaults along the way in.

As mentioned — destructuring is not necessary for known static APIs. But, any data, even some config that you think of as static, is worth destructuring, with defaults.

Using destructuring

Let’s see what we can do with destructuring:

This function utilizes destructuring features in different ways. Let’s analyze:

  • Setting defaults at the first/highest level (argument list) makes sure we can’t crash for empty arguments: func(). (This is “default parameters” that nicely work the same way as destructuring defaults).
  • Rename long key names for more readable logic when using the value (msg).
  • Use nested destructuring if you only need what’s inside a member value. But again, remember to also add a default to make the destructuring safe from crashing should the member value not exist.
  • It is powerful to also use the rest operator to be able to handle everything that was not explicitly destructured.
  • If we both need to use the whole structure and values inside, we destructure in the function scope (otherwise in the function header — as early as possible).
  • Both destructuring & the rest operator can be used for arrays as well.
  • We don’t have to repeat a long expression options.pos.x for a value if it’s used multiple times in the scope. Instead, just x. Cleaner, safer and also more performant in that JS doesn’t have to dig out the value each time.

This was an example of preparing for…

A function body

Consider that you in some function body need:

profile.user.address.street.name
profile.user.address.city

Say that this is highly dynamic data, and you want to do something with them, concatenate perhaps. Without destructuring you’d type:

`${profile?.user?.address?.street?.name ?? “Unknown street”} ${profile?.user?.address?.city ?? “Unknown city”}`

Not very pleasant(?) It is a mix of:

  • What we operate on
  • How we operate on it

With destructuring, made as early as possible in a suitable way for the situation:

  • You have declaratively made the unpacking of the stuff that is important to the logic in a separate step.
  • If you did the destructuring in the function argument list, only the values that is needed for the logic is available, nothing else. This also adds to why destructuring could be considered safer. You’re controlling what’s in the scope.
  • You know that you have safe, short values to work with, also easily usable multiple times in the scope. This is positive for the clarity of the logic.

So in your function body you now type:

`${streetName} ${city}`

Conclusion

What this article suggests as a habit:

  • Every time you type a dot .stop and think about if this is data that you’re operating on. If so — use destructuring instead.
  • Destructure as early as possible.
  • Remember to include default values, especially in nested destructuring.

I know... We have to address the elephant in the room:

Null in JavaScript

The destructuring version of the small example function is not strictly the same as with optional chaining & nullish coalescing. null is the value that will behave differently.

But it is not strictly destructuring that makes the example throw an error, it is default parameters (introduced in ES2015/ES6). Some put it this way:

null breaks default parameters.

However, destructuring defaults work the same way, which means they also only fall back to the defaults for undefinednot for null.

This is actually because it is the correct way

  • undefined in JavaScript means that the value is not yet set, it is not defined. All uninitialized values get the value undefined at run time.
  • null is an identifier that means that the value is actually set, but for some reason it could not get a proper value. Historically, null was included in the language for when one wanted to clear a variable containing (referencing) an object and let garbage collection destroy it. That’s why its type is "object". null is different from undefined and does not mean the same thing (and should not in your code).

undefined and null does not mean the same thing.

Acknowledging the meanings & differences between these two, both default parameters & destructuring work as intended.

I, along with Douglas Crockford, the Typescript team, Eric Elliot & many others, recommend that null should simply not be used in JavaScript (always use undefined instead, and use ESLint to help avoid it: https://www.npmjs.com/package/eslint-plugin-no-null).

null should not be used in JavaScript.

But that is subject for another article.

--

--

Esbjörn Blomquist

Software architect and developer with web and functional JavaScript close to his heart. Worked many years with web solutions mainly within the IoT field.