TypeScript and JavaScript conditional chaining; a conditional trap?

TypeScript and JavaScript conditional chaining; a conditional trap?

July 29, 2020β†’5 min read

An interesting post from Kent Dodds popped up on my twitter feed today, discussing a certain experience he had with one of the JavaScript shortcuts in a production environment, and the result not quite living up to expectation.

While he described at length what he encountered, how he resolved it and turned it into a learning opportunity, there was an interesting discussion around optional chaining.

What exactly is conditional chaining?

The phrase β€œconditional chaining” is a misnomer used to describe two features - optional chaining and non-null assertion chaining. Both don’t yet exist (optional does) in JavaScript, but can be provided through TypeScript (excluding known compatibility issues with different browsers).

The use of conditional chaining and non-null assertion chaining is very similar to the way that one would use conditional operators in a branching statement:

Ternaries:

const fooName = bar && bar.name ? bar.name : undefined; // union, non-null assertion
const fooName = bar === null || bar === void 0 ? undefined : bar.name; // intersection, optional assertion

Conditional chaining:

const fooName = bar!.name; // non-null assertion
const fooName = bar?.name; // optional chaining

Ok, so what’s optional chaining?

The optional chaining operator (?.) permits the deep reading of of properties located within a chain of connected objects, without having the recursively check the existence of the chain of objects before accessing to property.

Instead of causing an error when the chain encounters a nullish reference, the expression shortcuts to return undefined.

Silver bullet? Not exactly. There are still validation checks that are performed by the browser to ensure that the next chained object, or property, immediately following the optional operator exist. Otherwise you run into all sorts of undefined shenanigans.

Take the following code that may be written without optional chaining:

if (foo && foo.name) {
  ...
}
if (!!foo && foo.hasOwnProperty(name)) {
  ...
}

Let’s introduce optional chaining:

if (foo?.name) {
  ...
}

Looks great does it? It’s in the eye of the beholder, because with syntactic sugar like this, you still get drawbacks, such as reduced visibility of non-null assertions during property access, and return types that may be unexpected.

Tell me about non-null assertions

Just like && and || are inverse in conditional branching, ?. and !. are inverse when traversing a chain of references. While optional chaining will return undefined or the value of the final reference, the non-null assertion is a syntactic feature of TypeScript that will assert that a property is non-null during compilation, but will not ensure that it is not non-null at runtime.

A simple example:

// Non-null assertion (property!)
type fooBar = { name: string | null };
const foo: fooBar = {
  name: null
};

let s = foo!.name; // s has type 'string'
const p = `name: ${s}`;
console.log(p); // Will output name: null

Pitfalls

It’s important to note that at the time of writing, non-null assertions are still a feature of TypeScript, and optional chaining has limited browser support.

It’s always better to err on the side of caution when using syntactic sugar features that aren’t widely supported, even when using a superset of JavaScript such as TypeScript. Take for instance an ES3/ES5 targeted version of the ?. operator:

type fooBar = { name: string | null };
const foo: fooBar = {
  name: null
};

let s = foo?.name;
const p = `name: ${s}`;
console.log(p);
var foo = {
    name: null
};
var s = foo === null || foo === void 0 ? void 0 : foo.name;
// var s = foo.name; with non-null assertions
var p = "name: " + s;
console.log(p); // Will output name: null

The only difference between optional chaining and non-null assertion chaining is that optional chaining in TypeScript will be broken out to validate the individual components, where non-null assertion chaining will remove the operators.

The compilation of optional chaining becomes a mess with TypeScript the deeper you traverse the reference tree:

const foo: any = {}
let s = foo?.bar?.qwerty;

Becomes:

var foo = {};
var s = (_a = foo === null || foo === void 0 ? void 0 : foo.bar) === null || _a === void 0 ? void 0 : _a.qwerty;

And so on. We can’t reliably use these features natively in JavaScript yet without targeting specific versions of a given browser, but we can use the TypeScript ES target option to generate the next-best use.

Should I use conditional chaining

This is where objective turns to subjective. First and foremost, it’s syntactic sugar, a shorthand way of checking if a reference exists before accessing it, to avoid the dreaded undefined error:

const foo = {};
if (foo.name) {
  ...
}
// outputs 'undefined' to the browser console

There’s a clear, tangible and measurable benefit for using it, and a reason to use it, however one must consider the use of conditional chaining in traversing data structures, especially when the validation of references has been obfuscated, and the behaviour differs between the operators.

Non null assertion does not error out, nor is it compiled to validate the truthiness of a reference before accessing it, it simply transforms foo!.name into foo.name and if it can guess the type that it’s supposed to work with at compilation type, maybe return an error. All of that can be easily voided using any even when compiling with --strictNullCheck.

Optional chaining intentionally hides the reference validation from the user, and it’s definitely helpful and useful. But it removes often necessary truthiness checks from code, to replace with a character that can be easily confused or missed, especially given that it’s similar in appearance to a ternary operator, especially a minified instance:

var a=_b?_b.a;undefined;
var a=b?.a;

It’s definitely one to consider before adopting it widescale.