DEV Community

Valentino Gagliardi
Valentino Gagliardi

Posted on • Originally published at valentinog.com

Making Friends With Optional Chaining in TypeScript

Originally published on my blog.

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 beta.

Setting up TypeScript

First things first create a new project and install TypeScript beta:

mkdir optional_chaining_ts && cd $_

npm init -y

npm i typescript@beta
Enter fullscreen mode Exit fullscreen mode

Next up generate a configuration file for TypeScript:

node_modules/typescript/bin/tsc --init
Enter fullscreen mode Exit fullscreen mode

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" }
];
Enter fullscreen mode Exit fullscreen mode

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;
});
Enter fullscreen mode Exit fullscreen mode

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 ]
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

What can be done to protect our code from these kind of errors? Let's see if TypeScript can help.

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
Enter fullscreen mode Exit fullscreen mode

You should see the following error:

optional-chaining.ts:13:18 - error TS2532: Object is possibly 'undefined'.

13 const notThere = withCode[3].code;
                    ~~~~~~~~~~~
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

Notice in particular these line of code:

var _a;
var notThere = (_a = withCode[3]) === null || _a === void 0 ? void 0 : _a.code;
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

Thanks for reading and stay tuned.

Resources

New to TypeScript? Learn more with TypeScript Tutorial For Beginners: The Missing Guide.

More on optional chaining here.

Oldest comments (1)

Collapse
 
dlukanin profile image
Dmitry Lukanin

Just want to add - if you are not typescript lover you can always use lodash get method or some similar solutions.