70

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;
}
Servy
  • 202,030
  • 26
  • 332
  • 449
Ian
  • 33,605
  • 26
  • 118
  • 198
  • 16
    Let's rewrite some JavaScript engines! – JNYRanger Jul 24 '15 at 12:46
  • 1
    Related: http://stackoverflow.com/questions/476436/is-there-a-null-coalescing-operator-in-javascript – poke Jul 24 '15 at 12:47
  • 5
    You could `var length = customers && customers.orders && customers.orders.length`; – xanatos Jul 24 '15 at 12:49
  • 1
    Aactually, jnyranger might be on to something. Write a pre processing script to extend the js syntax. – Nick Bailey Jul 24 '15 at 13:03
  • Maybe lodash's [`_.get()`](https://lodash.com/docs#get) can help: `var customer = null, length = _.get(customer, "orders.length", null); // length -> null` [fiddle](http://jsfiddle.net/xxrrckxg/) – Andreas Jul 24 '15 at 13:03
  • @NickBailey I'm actually quite liking that idea too come to think of it, other answers are so verbose... – Ian Jul 24 '15 at 13:24
  • 3
    This feature might come in ES2016 or ES2017. There have been several discussions about this but no consensus yet afaik. Also, you don't have to write a JS engine, you can write a [Babel](https://babeljs.io) plugin. – Felix Kling Jul 24 '15 at 13:26
  • @FelixKling: Good shout, I've been meaning to look at Babel, writing a plug-in might be quite fun. That might be worth adding as an answer – Ian Jul 24 '15 at 13:29
  • 2
    This is the latest discussion is found: https://esdiscuss.org/topic/existential-operator-null-propagation-operator . But I don't think there is a proposal yet. – Felix Kling Jul 24 '15 at 13:34
  • 1
    While this is framework-specific and of trivial interest, [Angular2 does provide a template-level `?.`](http://stackoverflow.com/a/35073334/114900) – msanford Dec 16 '16 at 14:44
  • 1
    It exists in 2020! – djsoteric Apr 07 '20 at 05:44

4 Answers4

45

Called "optional chaining", it's currently a TC39 proposal in Stage 4. A Babel plugin 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
Brent L
  • 761
  • 7
  • 5
  • It's almost there: [available](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining#Browser_compatibility) in Chrome 80 and Firefox 74, Safari is lagging behind. – user Mar 09 '20 at 00:37
16

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.

RayLoveless
  • 19,880
  • 21
  • 76
  • 94
Uladzislaŭ
  • 1,680
  • 10
  • 13
  • Thanks - I could, but what I'd like to try and do is get to something that's much less verbose. – Ian Jul 24 '15 at 13:31
  • @Ian, maybe, there is smth in that really long [list](https://github.com/jashkenas/coffeescript/wiki/list-of-languages-that-compile-to-JS) with `null-coalesce` operator and js backward compatibility. I've hoped for type script, but there is not support yet. coffee script has `elvis-operator`, but its syntax and paradigm is too different from js. – Uladzislaŭ Jul 24 '15 at 13:44
  • @Ian, added one more option in my answer. – Uladzislaŭ Jul 24 '15 at 14:11
  • Yeah, that update is where my head was going when I was thinking about the problem. It's the most concise, it's just hard to read - which is where I wondered about any utilities that already existed or other automagical approaches. – Ian Jul 24 '15 at 14:12
  • 2
    I think that last should be `((customers || {}).orders || []).length` (an empty array after the last `||`). – poke Jul 24 '15 at 14:24
  • @poke it haven't been specified that orders is `array`, so :) – Uladzislaŭ Jul 24 '15 at 14:29
  • @Ian, what about functional approach? longer, but clearer as for me. updated answer one more time (the last) – Uladzislaŭ Jul 24 '15 at 14:30
  • @Vladislav heh, fair enough ;P – poke Jul 24 '15 at 14:48
  • @poke btw [].length will return 0, and you wont be able to distinguish it from empty orders collection. While ({}).length will propagate undefined as required and it's a correct one, I think. – Uladzislaŭ Jul 24 '15 at 15:07
  • What is "maybe monad"? Is that an "Else Heart.Break()" reference I'm not getting? – Protector one Aug 24 '16 at 11:13
6

There are several ways to improve code readability (depending on your needs):

  1. You already use (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"] }
    
  2. You already use (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);
    
  3. 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;
    
Mariano Desanze
  • 7,847
  • 7
  • 46
  • 67
1

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
J Bryan Price
  • 1,364
  • 12
  • 17