Is it native JavaScript? Isn't it??
My first encounter with optional chaining and nullish coalescing operator.
Last updated: July 23, 2022The first time I used Gatsby, I spotted some strange code snippet.
src/components/seo.jsJSX
1const defaultTitle = site.siteMetadata?.title
I wondered, "what's with that question mark after dot? Is it some special Gatsby syntax?" That didn't make sense to me, so I started looking for more information. After some research and Googling stupid questions, I found the term I was looking for - optional chaining. I was surprised - it is a native JavaScript. This syntax was introduced in ES2020 (on the day of writing this post, it is at stage four of the proposal process). First, we look at some errors to see why it's useful.
39nth Technical Committee (TC39) is a group under ECMA International that contain JavaScript developers, implementers, and academics. The committee collaborates with the community to maintain and evolve ECMAScript specification (JavaScript conforms to that specification). The development process has four stages. The fourth stage means the feature is ready to be included in the last draft of the specification.
If you're not a complete JavaScript beginner, you've probably seen a message like this: "Cannot read properties of undefined." It usually means that you want to access a property of a nested object that doesn't exist. Let's reproduce this error, but this time on purpose. Imagine you want to display detailed information about computers - laptops and desktops. They almost consist of the same parts, but some laptops don't have a dedicated graphics card - there is no information about it. If you try to access that information, you'll get our error.
JS
1const desktop = {2 processor: {3 manufacturer: 'Intel',4 type: 'I7'5 },6 graphicsCard: {7 manufacturer: 'Nvidia'8 }9 //...more components10}1112const laptop = {13 processor: {14 manufacturer: 'AMD',15 type: 'Ryzen 5'16 }17 //...more components without graphics card18}1920const info = laptop.graphicsCard.manufacturer21//Uncaught TypeError: Cannot read properties of undefined (reading 'manufacturer')
Let's try to solve that error. First of all, we can fill in that information.
JS
1const laptop = {2 processor: {3 manufacturer: 'AMD',4 type: 'Ryzen 5'5 },6 graphicsCard: {7 manufacturer: ''8 }9}1011const info = laptop.graphicsCard.manufacturer //No error. Returns ""
But what, when we couldn't? We don't always have control over API. Another problem is when an object has more properties - every one of them should be an empty string
, null
, etc. Not ideal. JavaScript is a dynamic programming language, so there can be null
or undefined
values. We need to deal with situations like this. We can use a logical expression to solve this.
JS
1const laptop = {2 processor: {3 manufacturer: 'AMD',4 type: 'Ryzen 5'5 }6}78const info = laptop && laptop.graphicsCard && laptop.graphicsCard.manufacturer //undefined
It works. But it is verbose and clunky. We're only accessing one property, and we need two logical operators. Imagine there were many of them. Maybe conditionals will help.
JS
1const laptop = {2 processor: {3 manufacturer: 'AMD',4 type: 'Ryzen 5'5 }6}78const info = laptop9 ? undefined10 : laptop.graphicsCard11 ? undefined12 : laptop.graphicsCard.manufacturer //undefined
Was I writing something about clunky code? That looks even worse. Nested ternary operators are rarely a good idea. But JavaScript has syntax for error handling. Let's try...to catch this error.
JS
1const laptop = {2 processor: {3 manufacturer: 'AMD',4 type: 'Ryzen 5'5 }6}78let info9try {10 info = laptop.graphicsCard.manufacturer11} catch (error) {12 info = undefined //undefined13}
It is more readable, but it is still verbose. We're defining new scopes between braces and we can't use const
. It's time to solve this problem like a real, JavaScript developer - let's use a third-party library!
JS
1const R = require('ramda')23const laptop = {4 processor: {5 manufacturer: 'AMD',6 type: 'Ryzen 5'7 }8}910const info = R.path(['graphicsCard', 'manufacturer'], laptop) //undefined
This snippet is concise and readable. But do we need to use a third-party library for something basic like this? No, not anymore. Optional chaining comes to the rescue!
JS
1const laptop = {2 processor: {3 manufacturer: 'AMD',4 type: 'Ryzen 5'5 }6}78const info = laptop?.graphicsCard?.manufacturer //undefined
With optional chaining, you can try to access nested properties that may not be available. The above snippet won't throw an error. It will return undefined
. If a reference is null
or undefined
, the expression returns undefined
. In my opinion, the ?.
operator is concise and intuitive. Is like a question: "Is there a manufacturer (of the graphics card) property on the laptop object?". If so - return it. Otherwise, return undefined
. This operator is more powerful. You can also use the syntax with function calls.
JS
1const laptop = {2 processor: {3 manufacturer: 'AMD',4 type: 'Ryzen 5'5 }6}78const info = laptop.nonExistingMethod?.() //undefined
The object has not any method. Still, the snippet above doesn't throw an exception. It returns undefined
. Let's go further - can you use this operator with arrays? Yes, you can.
JS
1const laptop = {2 ram: ['Kingston 8GB', 'Kingston 8GB']3}45const info = laptop.ram?.[3] //undefined
Even though there are no four elements in the array, the code doesn't throw an error.
Nullish coalescing operator
If we "add" another question mark to our operator and remove the dot, we get a new logical operator - the nullish coalescing operator.
JS
1const laptop = {2 processor: {3 type: 'Ryzen 5'4 }5}67const info = laptop.graphicsCard?.manufacturer ?? 'integrated' //integrated
You can interpret the above snippet like this: "If a graphics card manufacturer in the laptop object exists, assign its value to info variable. Otherwise, assign the integrated string." From the previous section, we know that our optional chaining returns undefined
. Then interpreter moves to the logical operator. The nullish coalescing operator is a specific case of logical OR. It returns the right-hand side operand when its left-hand side operand is nullish (null
or undefined
). Logical OR returns the right-hand side operand if the left-hand side operand is any falsy
value.
JS
1const a = null ?? 'default' //"default"2const b = undefined ?? 'default' //"default"3const c = '' ?? 'default' //""4const d = NaN ?? 'default' //NaN - it returns every other falsy value56const e = '' || 'default' //"default"7const f = NaN || 'default' //"default"8const g = 0 || 'default' //"default" - it returns "default" for falsy values
The behavior of logical OR can lead to unexpected errors. For example, if you want a default number, but 0 is a correct, expected value. That's why the nullish coalescing operator is handy. It is stricter and can prevent bugs like this. But it does not replace logical OR. Also, you can't chain the nullish coalescing operator with other logical operators without parenthesis.
null || undefined ?? "default"
The above code raises a syntax error.
(null || undefined) ?? "default"
The above code is correct and returns "default.”
Bonus - logical nullish assignment
While researching this post, I found one more operator with a double question mark. If we "add" an equal sign to the nullish coalescing operator, we get the logical nullish assignment. It is easy to deduce what it is doing. Instead of returning, it assigns a particular value to x if x is nullish.
JS
1const laptop = {2 processor: 'Intel'3}45laptop.processor ??= 'AMD'6laptop.graphicsCard ??= 'Nvidia'78console.log(laptop.processor) //"Intel"9console.log(laptop.graphicsCard) //"Nvidia"
It is a shortcut for the expression with the nullish coalescing operator. The two expressions below are equivalents.
JS
1const laptop = {2 processor: 'Intel'3}45laptop.graphicsCard ??= 'Nvidia'6laptop.graphicsCard ?? (laptop.graphicsCard = 'Nvidia')7//Above two expressions do the same thing89console.log(laptop.graphicsCard) //"Nvidia"
If you want more examples or details, visit MDN Web Docs: