Surprises in optional chaining

So you have a bunch of foo && foo.bars lying around and you decide it’s time: you want to convert your JavaScript codebase to use optional chaining. You can’t wait to gaze upon all those foo?.bars with pride and excitement. You might think that you can just grep /([a-zA-Z]+)(?: |\n)+&&(?: |\n)+\1./$1?./ and be done, but, as I’ve discovered, it’s not quite that easy.

There are a few intrinsic properties of the optional chaining operator that might surprise you. Some of these might seem obvious, but it’s worth spelling them out, just in case you might have written the same bug I would have. 😉

What even is optional chaining

MDN is great, and their entry on the optional chaining operator is also great.

It’s definitely worth a quick skim if you’re not familiar, but even if you are familiar, it’s worth taking a look; it can be applied in some potentially surprising ways, like:

  • array access: foo?.[1] ; foo?.[bar]
  • function access: something?.()
  • short-circuiting expressions: thing?.[(expressionPotentiallyNeverInvoked)];)

Negated conditions cannot be directly replaced

foo && foo.bar === 'baz' can be represented as foo?.bar === 'baz'. foo && foo.bar !== 'baz' cannot be replaced with foo?.bar !== 'baz'.

+—————————-+————-+
|         condition          | truth value |
+—————————-+————-+
| foo does not exist         | false       |
| foo.bar equals baz         | true        |
| foo.bar does not equal baz | false       |
+—————————-+————-+

but you’ve replaced it with this one:

+—————————-+————-+
|         condition          | truth value |
+—————————-+————-+
| foo does not exist         | true        |
| foo.bar equals baz         | true        |
| foo.bar does not equal baz | false       |
+—————————-+————-+

undefined is not null

Shout-out to Chris Morgan, who pointed this out to me nearly three years ago!

undefined is the absence of a value. null is the intentional absence of a value. Maybe people regard the inclusion of both in the language as a mistake, but the distinction is often useful. Overture, Fastmail’s JavaScript framework, uses null as a signal a lot.

null?.something evaluates to undefined.

If you want to take a blanket approach and use the optional chaining operator as much as possible, you have to be confident that you never check to see if this value is explicitly null downstream. Of course, you likely aren’t taking advantage of special properties of null either, like how null > -1 === true but undefined > -1 === false. At least, you don’t think you do. Right? Are you sure? 🙂

Speaking of false…

Say you have foo && foo.bar(). Say you’re not using TypeScript, but say foo can be a boolean or a function. If foo is false, foo?.bar() is a TypeError.

If you’re not using TypeScript, it’s not a small amount of work to go through and make sure you’re not writing this bug.

You can short-circuit expressions early

MDN touches on this, but it bears spelling out: foo && foo.bar && foo.baz can be replaced with foo?.bar && foo.baz.

But: should you? Optional chaining hasn’t really fully sunk into my consciousness yet. I don’t feel like I miss other one-character operators when reading code — !s in expressions are totally fine. Being tripped up on foo?.bar && foo.baz can’t lead to any real misreadings of the code, either, other than maybe needing to take a second look.