Null Conditional Operators
JavascriptJavascript Problem Overview
C# 6.0 has just been released and has a new nice little feature that I'd really like to use in JavaScript. They're called Null-conditional operators. These use a ?.
or ?[]
syntax.
What these do is essentially allow you to check that the object you've got isn't null
, before trying to access a property. If the object is null
, then you'll get null
as the result of your property access instead.
int? length = customers?.Length;
So here int
can be null, and will take that value if customers
is null. What is even better is that you can chain these:
int? length = customers?.orders?.Length;
I don't believe we can do this in JavaScript, but I'm wondering what's the neatest way of doing something similar. Generally I find chaining if
blocks difficult to read:
var length = null;
if(customers && customers.orders) {
length = customers.orders.length;
}
Javascript Solutions
Solution 1 - Javascript
Called "optional chaining", it's currently a [TC39 proposal in Stage 4][2]. A [Babel plugin][3] however is already available in v7.
Example usage:
const obj = {
foo: {
bar: {
baz: 42,
},
},
};
const baz = obj?.foo?.bar?.baz; // 42
const safe = obj?.qux?.baz; // undefined
[2]: https://github.com/tc39/proposal-optional-chaining "TC39 proposal in Stage 3" [3]: https://github.com/babel/babel/releases/tag/v7.0.0-alpha.15 "Babel plugin"
Solution 2 - Javascript
Js logical operators return not true
or false
, but truly
or falsy
value itself. For example in expression x && y
, if x
is falsy, then it will be returned, otherwise y
will be returned. So the truth table for operator is correct.
In your case you could use expression customers && customers.orders && customers.orders.Length
to get length
value or the first falsy
one.
Also you can do some magic like ((customers || {}).orders || {}).length
(Personally, I don't like this syntax and possible garbage collection pressure as well)
Or even use maybe
monad.
function Option(value) {
this.value = value;
this.hasValue = !!value;
}
Option.prototype.map = function(s) {
return this.hasValue
? new Option(this.value[s])
: this;
}
Option.prototype.valueOrNull = function() {
return this.hasValue ? this.value : null;
}
var length =
new Option(customers)
.map("orders")
.map("length")
.valueOrNull();
It's longer than all the previous approaches, but clearly shows your intentions without any magic behind.
Solution 3 - Javascript
There are several ways to improve code readability (depending on your needs):
-
You already use [tag:BabelJS] (v7 or above) and you use "Optional Chaining" babel plugin (finished proposal) (or just preset-stage-3):
const length = customers?.orders?.Length; // With default value (otherwise it will be `undefined`): const length = customers?.orders?.Length || defaultLength; // File: .babelrc { "plugins": ["@babel/plugin-proposal-optional-chaining"] }
-
You already use [tag:lodash] (v3.7 or greater): Use the
lodash.get
method:var length = _.get(customers, 'orders.length'); // With default value (otherwise it will be `undefined`): var length = _.get(customers, 'orders.length', defaultLength);
-
Plain javascript:
var length = customers && customers.orders && customers.orders.length; // With default value (otherwise it may be whatever falsy value "customers" or "customers.orders" might have): var length = (customers && customers.orders && customers.orders.length) || defaultLength;
Solution 4 - Javascript
Here's a quick and dirty version that works.
String.prototype.nullSafe = function() {
return eval('var x='+this.replace(/\?/g,';if(x)x=x')+';x');
};
Example usage:
var obj = {
Jim: 1,
Bob: { "1": "B", "2": "o", "3": "b" },
Joe: [ 1, 2, 3, { a: 20 } ]
};
obj.Joe[3].a // 20
"obj.Joe[3]?.a".nullSafe() // 20
obj.Joe[4].a // Error: Can't read 'a' from undefined
"obj.Joe[4].a".nullSafe() // Error: Can't read 'a' from undefined
"obj.Joe[4]?.a".nullSafe() // undefined
obj.Jack[3].a.b // Error: Can't read '3' from undefined
"obj.Jack[3].a.b".nullSafe() // Error: Can't read '3' from undefined
"obj.Jack?[3].a.b".nullSafe() // undefined