DEV Community

Cover image for 😰 Optional chaining trap !
Antoine Caron
Antoine Caron

Posted on

😰 Optional chaining trap !

A few days ago, an announcement that many expected was published in TC39 Stage 3. Optional Chaining

Example here with

It is a great news for Javascript ! This feature is awesome but...

Please agree with me, this feature will not be available tomorrow in our browsers. In stage 3, it is very likely that this feature will be added in the next release of ES.

But

We will need to babel it for a very long time.

If you take a look at @babel/plugin-proposal-optional-chaining, this is how babel will transpile it.

babel transpile example

This is kind of a very verbose output.

Let's imagine that we use this feature very many times in a web application, and you use it for deep case.

const foo = everything?.could?.be.nullable?.maybe

// Babel will generate this output

var _everything, _everything$could, _everything$could$be$;

var foo = (_everything = everything) === null || _everything === void 0 ? void 0 : (_everything$could = _everything.could) === null || _everything$could === void 0 ? void 0 : (_everything$could$be$ = _everything$could.be.nullable) === null || _everything$could$be$ === void 0 ? void 0 : _everything$could$be$.maybe;

// Terser would modify like this
l,n,o;null===(l=everything)||void 0===l||null===(n=l.could)||void 0===n||null===(o=n.be.nullable)||void 0===o||o.maybe
Enter fullscreen mode Exit fullscreen mode

It's going to be really verbose in your bundles. The transformation made by babel in the state does not at all share the nullsafe access mechanism as lodash.get can do. Even if lodash is very/too heavy. It offers a more efficient nullsafe implementation while generating less code.

You're going to tell me,

"What the heck! Antoine, it's not the first time we've used a not-so-great polyfill to be able to use a new feature of EcmaScript"

Yeah

Ok but this time we can still look a few minutes to propose an implementation of a less trivial polyfill. This solution cannot really be applied in a world where the web developer turns into a Ko hunter.

Let's look at how lodash.get works. Github link

import castPath from './castPath.js'
import toKey from './toKey.js'

/**
 * The base implementation of `get` without support for default values.
 *
 * @private
 * @param {Object} object The object to query.
 * @param {Array|string} path The path of the property to get.
 * @returns {*} Returns the resolved value.
 */
function baseGet(object, path) {
  path = castPath(path, object)

  let index = 0
  const length = path.length

  while (object != null && index < length) {
    object = object[toKey(path[index++])]
  }
  return (index && index == length) ? object : undefined
}

export default baseGet
Enter fullscreen mode Exit fullscreen mode

It's very effective (very compact). What if the polyfill of the Optional Chaining involved the application of a utility function like lodash.get?

What's your opinion on that?

Feel free to share and react if you liked this article.

Top comments (34)

Collapse
 
thekashey profile image
Anton Korzunov

I hate this pattern. I hate lodash getter, and I hate optional chaining.

You have to answer the question β€œwhy” some key is empty, and what you are going to do then - handle the root cause, not the consequences.

Collapse
 
woubuc profile image
Wouter • Edited

When you're working with data coming in from an external source (user input, rest api, database, ...) you can never be 100% certain that it won't be null.

Data structures inside your application should indeed be designed to always adhere to the same strict schema, although even that isn't always the case.

Also: that's a very strong reaction to have to a piece of programming syntax.

Collapse
 
slashgear_ profile image
Antoine Caron

I live in a world where

Everything πŸ‘could πŸ‘be πŸ‘ nullable πŸ‘

I really think that being able to design an application where nothing is null is a utopia.

Collapse
 
elmuerte profile image
Michiel Hendriks

I like the safe navigation operator, but I fear its usage. One level is ok, two might be acceptable, but beyond that you are doing things wrong.

It is nice for templates. But for normal programming? What are you doing so deep in an object structure?

Collapse
 
woubuc profile image
Wouter

I agree, overuse of optional chaining and null-coalescing is something that code styles and guidelines will have to limit.

As I see it, it will mostly be useful for things like options objects loaded from a JSON or YML file, where you have settings that may or may not be set, and often 1-3 levels deep. Or when loading documents from a MongoDB where you have set of properties and data that may or may not be there.

Collapse
 
thekashey profile image
Anton Korzunov

What are you doing so deep in an object structure?

Exactly the point! If a top level key is not accessible, then:

  • why it's not accessible? it is null, undefined, so you can't go deeper, or it string, and your data type is too mutable? It's a quite popular to have something which could be "string/object" or "object/function". Why?
  • then may be another keys from the same location, you are going to read a moment later is also not accessible. Why? May be some decision should be made a bit before? Why?
  • and of course - why some data (you got from the network) might unpredictable not exists, and you are not "ready" for this. Why?
Collapse
 
eddiecooro profile image
Eddie

Because it's some response coming from the server without any type safety, maybe.

Collapse
 
devinrhode2 profile image
Devin Rhode • Edited

what do people think of this debugging focused cousin of lodash.get, I call it "safeGet"

if (!window.safeGet) {
    // inspiration: https://dev.to/devinrhode2/comment/fcnp
    // I wish this was a polyfill on Object.prototype but that is just not working..
    // // I guess it's not technically a polyfill until I submit a proposal to tc39...
    // window.customObject_prototype_lookup_defined = true;
    // if (Object.prototype.lookup) {
    //     console.error(
    //         'Object.prototype.lookup is already set to:',
    //         Object.prototype.lookup,
    //         'this is not expected'
    //     );
    // }
    // // eslint-disable-next-line no-extend-native
    // Object.prototype.lookup = function Object_lookup(object, path) {
    //     let object = this;
    window.safeGet = function safeGet(object, path, { returnLastNonNullValue = false }) {
        path = path.split('.');

        let index = 0;
        const length = path.length;
        let currentPath = '';
        let nextBit = '';

        while (object != null && index < length) {
            nextBit = path[index++];
            // eslint-disable-next-line security/detect-object-injection
            if (object[nextBit] != null) {
                currentPath = currentPath + '.' + nextBit;
            } else {
                console.log(
                    'Lookup failed: ' + currentPath + '.' + nextBit + ' is:',
                    // eslint-disable-next-line security/detect-object-injection
                    object[nextBit],
                    currentPath + ' is:',
                    object
                );
            }
            if (returnLastNonNullValue) {
                // eslint-disable-next-line security/detect-object-injection
                if (object[nextBit] != null) {
                    // eslint-disable-next-line security/detect-object-injection
                    object = object[nextBit];
                }
            } else {
                // default lodash.get behavior
                // eslint-disable-next-line security/detect-object-injection
                object = object[nextBit];
            }
        }

        if (index && index == length) {
            // successful lookup
            return object;
        } else {
            // even if we didn't get to end of property path
            // returnLastNonNullValue may be true
            // therefore return object if it's not null
            return object != null ? object : undefined;
        }
    };
}
Collapse
 
devinrhode2 profile image
Devin Rhode • Edited

I'd really love this api:
someObject.lookup('some.really.long.property.path') - works essentially just like lodash.get or optional chaining. But what if you want to know why the lookup failed?
just print someObject.lastLookupMsg

As a failsafe, is the last lookup was successful.. this could be set to true (there is no meaningful message for successful lookups)
If the last lookup failed, it COULD just automatically console.log("Lookup failed: some.really.long is:", some.really.long, "some.really is:", some.really);

Maybe there could be a cousin to optional chaining ?. – perhaps ~. Means "tread carefully" and returns the last property lookup that was not undefined/null.

Or perhaps ?. could have a debugging version which does the console.log for you in development only.

Collapse
 
devinrhode2 profile image
Devin Rhode

At the end of the day I think I would prefer to use a simple Object.prototype.lookup method that automatically console.log's the message I described.

Thread Thread
 
devinrhode2 profile image
Devin Rhode • Edited

One issue is that passing an object lookup path as a string results in no syntax highlighting, you probably lose out on a bunch of other potential ide goodies, but from what I know they mostly don't apply to the main use case of check data from the user or from over the wire.

Collapse
 
devinrhode2 profile image
Devin Rhode • Edited

I wasn't able to add lookup to the Object.prototype because I was getting this error with my CRA v3+CRACO setup...

"Invariant Violation: EventPluginRegistry: Cannot inject event plugins that do not exist in the plugin ordering, lookup"

The stack trace was pure webpack hell, did not include it in my google search..

I tried adding it directly in my index.html in a script tag but that didn't fix it. I could, however, delay defining it for 5 seconds via setTimeout and I can then define Object.prototype.lookup

Collapse
 
stereobooster profile image
stereobooster

It's very effective.

This is ambiguous terminology (at least in this context). You mean it is very compact output. Not effective in sense performance. Most likely performance of verbose code will be better (you always need to measure to be sure).

babel plugin for github.com/facebookincubator/idx does the same.

Collapse
 
smeijer profile image
Stephan Meijer

idx works indeed similar, but it does provide a little bit less over overhead.

optional-chaining, 443 bytes vs idx, 254 bytes. As the original is only 83 bytes, they both come with an overhead.

I should be happy that optional chaining has reached stage 3. As we are using the proposal for quite some time now. But instead, I'm worried. I see it being heavily overused in some places, because it's so easy to use.

function CommentButtons({ user }) {
  return (
    <div>
      <Button disabled={user?.can?.reply}>reply</Button>
      <Button disabled={user?.can?.delete}>delete</Button>
    </div>
  )
}

Easy right? Both buttons are disabled if lacking the proper permissions. This however compiles down to 731 bytes and a lot of ternary operators, while we could simply reduce this down to something like:

function CommentButtons({ user, can = user ? user.can : {} }) {
  return (
    <div>
      <Button disabled={can.reply}>reply</Button>
      <Button disabled={can.delete}>delete</Button>
    </div>
  )
}

If it's your own code base. It's safe to make some assumptions. For example, to say that if the user is set, that it's guaranteed to have a can property wich is an object.

Collapse
 
wintercounter profile image
Victor Vincent • Edited

{ user: { can = {} } }

Thread Thread
 
smeijer profile image
Stephan Meijer

I'm aware of that syntax. But defaults don't work for null values. As grapql tends to return null for missing data, I don't use that syntax that much.

In a real world scenario, I would have moved the assignment out of the arguments.

Thread Thread
 
wintercounter profile image
Victor Vincent

Makes sense!

Collapse
 
devinrhode2 profile image
Devin Rhode

When browsers finally start shiping support for optional chaining.... the amount of bytes sent over the wire can decrease quite a bit.... but that's going to take some time right?

Collapse
 
smeijer profile image
Stephan Meijer

I took some time to create that peformance test you're talking about. But no, it doesn't perform better. Mostly, because it will check way more then required.

We as developers know that if user is not an object, that it doesn't meet our requirements. Babel will however check against null and void 0. While we could just check if it's "truthy". So the transpiled code contains at least twice as much checks as required.

I just published a small post, inspired by this one. If you're only interested in the performance, you can check the outcome here: jsperf.com/costs-of-optional-chaining.

Collapse
 
stereobooster profile image
stereobooster

I meant performance of babel-compiled optional chaining vs baseGet. What author talked about in the post

Collapse
 
slashgear_ profile image
Antoine Caron

Will change that, thanks :D

Collapse
 
adam_cyclones profile image
Adam Crockett πŸŒ€ • Edited

You can do this today with just a recursive proxy that returns an object if undefined.

Collapse
 
woubuc profile image
Wouter • Edited

One comment against this that I've read, is that the Javascript engine can optimise inline code better. Using a function like lodash.get would invoke an extra function call (creating a new function scope) and run more complex logic for every statement that uses optional chaining. I'm not all that familiar with runtime optimisations myself, but it sounds plausible to me that checking if foo === null is a lot less expensive than calling a function like lodash.get.

Also, frequently repeated patterns (like === null) can get optimised and pretty heavily with gzip compression, so I doubt it would increase the download size by that much. Especially compared to the other hundreds of kilobytes of dependencies we usually have in our frontend bundles.

Collapse
 
thekashey profile image
Anton Korzunov
  1. JS engine would try to optimize code locations(functions) which are often used.
  2. Feeding different data structures into the same function would eventually cause its deoptimization, and that would make it very slow.
Collapse
 
woubuc profile image
Wouter

I know the Javascript engine can do a lot, but I find it hard to believe that it could optimise a while-loop with object assignments (like the lodash.get function) to be as performant as foo === null checks.

Collapse
 
slashgear_ profile image
Antoine Caron

I agree with that, using a function call is not the optimise solution.

As you point out, the compression made by Gzip can compensate for the use of this plugin. However, I think that on a large web application, the entropy generated by these ternaries will still generate many KB.

For use with NodeJS, this polyfill remains a great solution.

Collapse
 
woubuc profile image
Wouter • Edited

I was trying to set up a little test case to see which one is smaller, and then I looked at lodash a bit more closely.

In the lodash.get function, they use two other Lodash functions: castPath and toKey. The castPath function uses isKey and stringToPath. And so on. So in the end, for that one function you're importing all of this: github.com/lodash/lodash/blob/4.4....

Of course there are probably better, more concise options than the Lodash implementation, so I set up a quick test case with just the one function body as listed in your post: github.com/woubuc/optional-chainin...

Even here, Babel still comes out ahead.

I know this test case is far from perfect, and if you notice any mistakes I made, be sure to let me know so we can keep this accurate. But it shows that gzip compression really does optimise away most of the size of the repeated inline if statements, along with some optimisations that Babel itself does (see the bundles/babel.js file, it creates intermediate variables).

So I still believe Babel is the better option, both in terms of performance and bundle size.

That's not to say that it won't add any size to your bundle, but then all polyfills do.

Collapse
 
devinrhode2 profile image
Devin Rhode

I'm very much in agreement with you, but for different reasons

that lodash _.get methods looks pretty good.. could also have extra functionality to tell you which property was undefined, to help with debugging

and that lodash method COULD be added to the Object.prototype... so you COULD do..

person.lookup('details.name.fullName')

Maybe person is defined but is missing the details object. Maybe there's no fullName property. An actual function call could tell you these things in a very elegant want.

Collapse
 
kayis profile image
K

I think it's interesting, but I don't know if it's really good.

On the one hand, most of my errors are related to the problem this proposal tries to solve.

On the other hand, I never checked how often I can get along with null/undefined and NOT crash.

I mean sure, in some UI code I just want to display a value and if I don't get it, I can display nothing and be done with it.

But when I need an ID to get something from a back-end?

Promises are eating my errors like nobodies business already, now this? I don't know XD

Collapse
 
wrldwzrd89 profile image
Eric Ahnell

My opinion is along the lines of the general consensus: Optional chaining sure is compact... but VERY ugly if a polyfill is needed, along with an implicit acceptance of the risk of NULL / undefined data, which I am personally opposed to. As syntactic sugar goes, it makes things easier - but the ugliness of polyfills for JS gives me pause on adopting it now.

Collapse
 
sebbdk profile image
Sebastian Vargr • Edited

Friends don’t let friends use lodash :)

This feature sounds rather pointless to me right now. I wonder what the performance implications of using something like this is.

Collapse
 
adam_cyclones profile image
Adam Crockett πŸŒ€ • Edited

Not a problem to me it's designed this way. I'm not seeing the problem?