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.
Use ternaries rather than && in JSX
— Kent C. Dodds πΏ (@kentcdodds) July 28, 2020
What problems can happen when you use && to conditionally render content in JSXhttps://t.co/HXrPy5cyue
Complete with screenshots of a bug I shipped to production at PayPal π
"You do you, just don't do bugs"
π«ππππ¦πππ¦π¦π«
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.