Optional Chaining Operator '?.'
Published July 16th, 2021 • ☕ 5 min read
Have you ever written code like this before?
// Let's assume, we got this from a server responseconst pokemon = {Glurak: {favoriteTemperature: "180°C",favoriteFood: "Pizza Diavolo",},Bisasam: {favoriteTemperature: "26°C",favoriteFood: "Hummus",},};if (pokemon && pokemon["Mewto"] && pokemon["Mewto"].favoriteFood) {feed(pokemon["Mewto"].favoriteFood);}// If you don't check whether `Mewto` exists, you// will encounter something like this://// TypeError: Cannot read property 'favoriteFood' of undefined
Maybe at some point you became annoyed enough to look for a better solution. Chances are, you came across lodash's (or underscore.js') popular get
helper. Maybe you've even built your own lodash get
equivalent.
In which case your code could look like this:
if (_.get(pokemon, "[Mewto].favoriteFood")) {feed(data.pokemon.favoriteFood);}
I would argue, that in most practical cases, assuming that the feed
function can handle nullish values in some way, it's cool to strip down even further:
feed(_.get(pokemon, "[Mewto].favoriteFood"));// or provide a fallback for nullish values:feed(__.get(pokemon, "[Mewto].favoriteFood", "Pasta"));
Neat! However, if you care about website performance and bundle size, at least lodash seems like a rather heavyweight solution for such a seemingly simple task. According to bundlephobia, standalone lodash.get
adds 1.8kb to your site when the dependency is minified and gzipped.
Instead, you can conveniently use the Nullish Coalescing operator (??
), which has been released simultaneously and has similar adoption. No wonder, as these both operators do complement one another perfectly.
feed(pokemon["Mewto"]?.favoriteFood ?? "Pasta");
Here is a slightly longer alternative without the Nullish Coalescing Operator ??
:
feed(pokemon["Mewto"]?.favoriteFood !== undefined && pokemon["Mewto"]?.favoriteFood !== null? pokemon["Mewto"].favoriteFood: "Pasta",);// slightly shorter version if truthy value is good enough// (this is functionally *not* the same as the one above):feed(pokemon["Mewto"]?.favoriteFood ? pokemon["Mewto"].favoriteFood : "Pasta");
The ?.
operator allows you to safely access an object property without checking if the parent is defined. It stops the evaluation if the value before ?.
is undefined
or null
and returns undefined
.
The Optional Chaining Operator cannot be used on a non-declared root object, but can be used
with an undefined root object. Hence we are declaring const pokemon
before in some way:
const pokemon = { ... }
Performance-wise, it's not a big deal either way. Although the optional chaining operator performs about 200% faster against lodash.get
, practically, your app would have to be written in a very uncommon way for it to make a noticable difference (that is due to the nature of the code using lodash.get
or Optional Chaining).
The cooler gain happens in bundle size (and you can save a few additional characters or even lines of code vs checking vanilla / using lodash.get
). Although it's just 1.8kb gzipped, if you've actively optimized your website loading speed before, you should know that it's a sweeping effort, and so saving another 1.8kb is another welcome gift.
That being said, there can be a relevant caveat in regards to bundle size, if you're using older Node.js versions or need to support a specific range of browsers. More on that a little further below.
In a perfect world we shouldn't need this kind of checks
In a perfectly coded world, we would go for the root cause of why an object property is missing and try to fix that. We would also be willing to throw errors anywhere in the application if something slipped by to handle it in a more intentional way and work towards solutions.
However, we're not living in such a world. Knowing, that network requests can fail in the most astonishing ways, we often just want to make things work. The fact, that this feature has made it into JavaScript, as well as the wide-spread usage of lodash.get
is indication enough to see, that this is needed and wanted in the real world.
It's good that we have the option to use the feature. Whether you do or not, and at what places you do it or not, is still your free decision. If you want to ban it from your company's codebase, I'm sure you can still come up with an ESLint rule for it.
Even if we accept, that we're living in a flawed world, it is good practice to use ?.
sparingly. Don't just blindly use it every time you go deeper in an object. Instead, take a moment
if an object or property should definitely exist. If you silence too much, you might silence
important errors and make debugging harder in the future.
Practical usage
You can use the Optional Chaining Operator at few occasions:
// access object properties with dot notation:obj.val?.prop;// access object properties with bracket notation:obj.val?.[expr];// access array items:obj.arr?.[index];// call function:obj.func?.(args);
Apart from accessing nested objects and properties, you can also call functions with ?.
.
If there is a property with such a name and which is not a function, using ?.
will still raise a TypeError
:
const foo = {bar: "not a function",};foo.missingMethod?.();// nothing happensfoo.bar?.();// TypeError: foo?.bar is not a function
Using with React.js
There is a common pattern we use in React.js, where the Optional Chaining Operator can help us shorten the code. Here is, what we can find in a lot of code bases:
const Component = () => {const [data, setData] = useState();React.useEffect(() => {// populate with setData() from network request}, []);return (<div>{data &&data.rows &&data.rows.map(row => {return <div className="title">{row.title}</div>;})}</div>);};
Here is how we can use ?.
to shorten the code and improve readability:
const Component = () => {const [data, setData] = useState();React.useEffect(() => {// populate with setData() from network request}, []);return (<div>{data?.rows.map(row => {return <div className="title">{row.title}</div>;})}</div>);};
Note, how I wrote data?.rows.map( ...
instead of data?.rows?.map( ...
.
That is, because we would expect rows to be an array, if it exists. If it does not exist, it
should mean only one of two things:
- The network request hasn't finished yet
The network request has finished and was erroneous. In this case, the error should be handled as part of the
useEffect
callback logic.
With this knowledge, if an error still occurs, it won't be silenced and we can inspect it (for
example, if the returned data.rows
from the server is not an array, we would
instantly know about it).
About backwards compatibility and bundle sizes
The TC39 proposal for Optional Chaining has reached stage 4 (which means finished and ready to be included into EcmaScript) in December 2019. Since then it has been a pretty long time in dev land, especially in the fast-paced JS state. Node.js 14 supports Optional Chaining and ~90% of browsers know how to handle it, too.
That means, that if you're depending on Node.js <= 13, or need to support those 10% of browsers specifically, you'll have to actively enable Optional Chaining in one way or the other. There is a --harmony
flag for older Node.js versions for example, or you could use a polyfill or something like @babel/plugin-proposal-optional-chaining
to transpile your code. Either way, just be aware that it will probably grow your bundle size in one or the other way.
Specifically if you're going to transpile the code, pay attention to how often you're using Optional Chaining (or really any transpilable code) throughout the application. That is, because each additional occurrence will grow your code lineary (but then again, gzip does a pretty good job here). On the other hand, if you include lodash.get
for example, it's just the cost of the module once, and (essentially) no further costs on each additional usage. I don't think this is a practically important consideration for 99.99% of all codebases out there, but if you're one of those rare exceptions I trust you to figure this out on your own.