Using Optional Chaining in TypeScript and JavaScript
JavaScript moves at a fast pace and so TypeScript, pushing new features into the language. Learn how to use optional chaining for cleaning up your code.
To be honest I never jump on newest JavaScript proposals so fast. If it's not at least at stage 3 most of the times I gloss over. But if the new feature is implemented in TypeScript then I know it's going to be good.
That's exactly the case with optional chaining in TypeScript. It will land into JavaScript and it's already available in TypeScript.
Optional chaining landed in Chrome 80.
Setting up TypeScript
First things first create a new project and install TypeScript:
mkdir optional_chaining_ts && cd $_
npm init -y
npm i typescript
Next up generate a configuration file for TypeScript:
node_modules/typescript/bin/tsc --init
Once done create a new JavaScript file and name it as you wish, I called mine optional_chaining.js
. And now let's see optional chaining in action.
The problem: map function and undefined
From now on we'll work inside optional_chaining.js
. Suppose you've got the following array:
const arr = [
{ code: "a" },
{ code: "b" },
{ code: "c" },
{ name: "Caty" },
{ name: "Siri" }
];
You want to loop over it to produce a new array containing only those objects with the code property. The map function is your friend and we can do:
const arr = [
{ code: "a" },
{ code: "b" },
{ code: "c" },
{ name: "Caty" },
{ name: "Siri" }
];
const withCode = arr.map(function(element) {
if (element.code) return element;
});
The only problem now is that we get undefined for every element where map couldn't find the code property. Here's the resulting array:
// withCode now is
[ { code: 'a' },
{ code: 'b' },
{ code: 'c' },
undefined,
undefined ]
At this point in JavaScript you would be free to access an empty index, or worst, a non-existing object:
const notThere = withCode[3].code;
Only at runtime your program will throw (or your JavaScript test suite will fail if you tested that edge case):
TypeError: Cannot read property 'code' of undefined
The problem exists more in general with property access on nested objects. Consider another example:
const people = { mary: { name: "Mary" } };
const caty = people.caty.name;
// TypeError: Cannot read property 'name' of undefined
What can we do to protect our code from these kind of errors? Let's see how optional chaining helps.
The solution: TypeScript and optional chaining
Let's get TypeScript to check our code. Rename optional_chaining.js
to optional_chaining.ts
. Then try to compile:
node_modules/typescript/bin/tsc
You should see the following error:
optional-chaining.ts:13:18 - error TS2532: Object is possibly 'undefined'.
13 const notThere = withCode[3].code;
~~~~~~~~~~~
Good catch TypeScript! How did you know? TypeScript sees that the statement if (element.code) return element;
could exclude objects whose properties don't have "code". And that will lead to undefined elements.
At this point we have two options. We can return an empty object like { name:"empty" }
as a fallback from the map function. But it could be bad for performance. Better, we could check if our object exists before accessing a key:
const notThere = withCode[3] && withCode[3].code;
What a hacky thing to do right? How many times did you see code like that? We had no choices until now.
With optional chaining instead we can clean up the code and reduce the check to:
const notThere = withCode[3]?.code;
If you followed along you should have this code (I've added a console log for printing notThere):
const arr = [
{ code: "a" },
{ code: "b" },
{ code: "c" },
{ name: "Caty" },
{ name: "Siri" }
];
const withCode = arr.map(function(element) {
if (element.code) return element;
});
const notThere = withCode[3]?.code;
console.log(notThere);
You can call it a day and go home now, but keep reading if you're interested in the nitty-gritty.
Optional chaining in TypeScript: how does it compiles?
Save, close the file and compile/run:
node_modules/typescript/bin/tsc
node optional-chaining.js
and you should see undefined
in the console. Still an empty value, but at least the code doesn't throw at runtime. How did we end up with undefined
by the way?
TypeScript takes the new syntax:
const notThere = withCode[3]?.code;
and compiles down to (assuming you're compiling to ECMAScript 2009):
"use strict";
var _a;
// omit
var notThere = (_a = withCode[3]) === null || _a === void 0 ? void 0 : _a.code;
console.log(notThere);
Notice in particular these line of code:
var _a;
var notThere = (_a = withCode[3]) === null || _a === void 0 ? void 0 : _a.code;
We can deconstruct them to plain english. The left part of the expression before ||
works like so:
Assign withCode[3]
to the variable _a
(declared in the head). Now check if _a
is equal to null
. If not, evaluate the right side of the logical or.
Not let's focus on the right edge of the expression after ||
.
It's a ternary operator stuffed with two void operators. The expression void 0 produces the undefined primitive. You can read the code like so:
If _a
is equal to undefined, then return undefined
, otherwise return _a.code
. In other words optional chaining always returns undefined when the value we're trying to access is non-existent, and property access on objects won't throw.
Wrapping up
JavaScript moves at a fast pace and so TypeScript, which pushes new feature and innovations forwards into the language. Optional chaining aims to simplify one of the most common patterns in JavaScript: nested property access on objects.
With optional chaining we can avoid TypeError in situations like the following:
const people = { mary: { name: "Mary" } };
const caty = people.caty.name;
// TypeError: Cannot read property 'name' of undefined
The same code with optional chaining becomes:
const people = { mary: { name: "Mary" } };
const caty = people.caty?.name;
// Instead of
// const caty = people.caty && people.caty.name;
Thanks for reading!