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.
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
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
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)
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.
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.
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.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?
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.
Exactly the point! If a top level
key
is not accessible, then:null
,undefined
, so you can't go deeper, or itstring
, and your data type is too mutable? It's a quite popular to have something which could be "string/object" or "object/function". Why?Because it's some response coming from the server without any type safety, maybe.
what do people think of this debugging focused cousin of lodash.get, I call it "safeGet"
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.
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.
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.
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
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.
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.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:
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 acan
property wich is anobject
.{ user: { can = {} } }
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.
Makes sense!
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?
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 anobject
, that it doesn't meet our requirements. Babel will however check againstnull
andvoid 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.
I meant performance of babel-compiled optional chaining vs baseGet. What author talked about in the post
Will change that, thanks :D
You can do this today with just a recursive proxy that returns an object if undefined.
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 iffoo === null
is a lot less expensive than calling a function likelodash.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.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 asfoo === null
checks.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.
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
andtoKey
. ThecastPath
function usesisKey
andstringToPath
. 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.
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.
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
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.
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.
Not a problem to me it's designed this way. I'm not seeing the problem?