Sibling-level optional chaining - new operator/syntax using pipe `|`

With optional chaining and nullish coalescing, we can achieve simpler and more shorthand syntax like this:

Before optional chaining and nullish coalescing

function(...params) {
  // ...
  return (obj1 && obj1.obj2 && obj1.obj2.arrProp && 
  obj1.obj2.arrProp[1] && obj1.obj2.arrProp[1].prop) || "fallback";
}

:point_up:t3: this can be shortened with optional chaining and nullish coalescing to the below, with a small but important difference re falsy vs. nullish values.

With optional chaining and nullish coalescing:

function(...params) {
  // ...
  return obj1?.obj2?.arrProp?.[1]?.prop ?? "fallback"
}

…but what if you want to reference another property if the first is nullish.

Referencing "fallback properties"

function(...params) {
  // ...
  return obj1?.obj2?.arrProp?.[1]?.prop1 ?? obj1?.obj2?.arrProp?.[1]?.prop2 ?? obj1?.obj2?.arrProp?.[1]?.prop3 ?? "fallback";
}

Or you may save the higher-level object and compare properties under it to simplify the expression, like this:

function(...params) {
  // ...
  const obj = obj1?.obj2?.arrProp?.[1];
  const { prop1, prop2, prop3 } = obj ?? {};

  return prop1 ?? prop2 ?? prop3 ?? "fallback";
}

I propose adding a new syntax for this to allow referencing fallback properties inline.

Fallback property syntax

function(...params) {
  // ...
  return obj1?.obj2?.arrProp?.[1]?.prop1.|prop2.|prop3 ?? "fallback";
}

Here, the pipe operator would as a sibling-level optional chaining operator. This can be used for the final property in an object path string or anywhere in the middle:

function(...params) {
  // ...
  return obj1?.subObj1.|subObj2.|subObj3?.arrProp?.[1].|[2].|[3]?.prop1.|prop2.|prop3.desiredValue ?? "fallback";
}

It's important to keep in mind that it should probably only be used mid-string when dealing with properties/elements that have the same structure as the rest of the path will still be used.

While this particular example may look complicated, it's meant to, in the heart of showing how powerfully it can be used. Destructuring an also look ugly in some opinions with enough nested layers and defaults, but used practically, it's a welcome and worthy feature in JavaScript.

To be clear, the above block would nbeed to be written like this (below) with existing syntax AFAIK:

function(...params) {
  // ...
  const fallback = "fallback";

  const { subObj1, subObj2, subObj3 } = obj1 ?? {};
  const subObj = subObj1 ?? subObj2 ?? subObj3;
  if (subObj == null) return fallback;

  const [elem1, elem2, elem3] = subObj.arrProp ?? [];
  const elem = elem1 ?? elem2 ?? elem3;
  if (elem == null) return fallback;

  const { prop1, prop2, prop3 } = elem ?? {};
  const prop = prop1 ?? prop2 ?? prop3;
  if (prop == null) return fallback;

  return prop.desiredValue ?? fallback;
}

At first glance, my initial thought is that this doesn't feel like something I (personally) really need that often, but perhaps if I paid attention as I coded, I'd find that it comes up more often than I think it does. I'll have to see.

My second thought was "I wonder how much pipelines help with this issue", so here's a comparison:

// with pipeline
function(...params) {
  // ...
  return obj1
    |> %?.subObj1 ?? %?.subObj2 ?? %?.subObj3
    |> %?.arrProp?.[0] ?? %?.arrProp?.[1] ?? %?.arrProp?.[2]
    |> %?.prop1 ?? %?.prop2 ?? %?.prop3
    |> %.desiredValue ?? "fallback";
}

// vs

// The proposed syntax (broken up over multiple lines)
function(...params) {
  // ...
  return obj1
    ?.subObj1.|subObj2.|subObj3
    ?.arrProp?.[1].|[2].|[3]
    ?.prop1.|prop2.|prop3
    .desiredValue ?? "fallback";
}

Which, the pipeline version certainly does help with the verbosity, as that's much more terse than the non-pipeline version of the function. But, the proposed syntax still helps a significant amount.

@theScottyJam Thanks for the feedback! I really appreciate the comparison.

Pipelines certainly are much better than no pipelines or this new syntax at all.

Looking between the two, I personally feel there's enough differentiation in usage between the two to justify the syntax, as someone may not want to jump straight into piping to simply reference one property or another.

I try to code with as much of a “stream of consciousness” as possible (the less critical thinking, the better when it can be avoided) and the new syntax I'm proposing just feels more intuitive and natural to me, and for that reason, I think it might be much easier to pick up and more fluid when using it in the day-to-day, if that makes sense or holds any weight.

I would also add that using the pipe operator specifically here feels pretty familiar and natural in my opinion, as it denotes both “or” as you are picking one property or the next, and so on.

The reason this syntax is rubbing me the wrong way is because I want to clearly see what object I'm trying multiple properties on. I think I'd be terrified to use this without parentheses for fear of order-of-operations messiness. I'd be more open to a syntax that groups the fallback keys together because they're more sibling-like, where a flat chain of . and .? are all parent-child relationships, if that makes sense.

Maybe instead, a method on Object.prototype? I'll call it tryKeys for the sec:

function(...params) {
  // ...
  return obj1
    ?.subObj1.tryKeys('subObj2','subObj3')
    ?.arrProp?.tryKeys(1, 2, 3)
    ?.tryKeys('prop1', 'prop2', 'prop3')
    .desiredValue ?? "fallback";
}
2 Likes

I think we’d be better off adding this to Object itself rather than its prototype, so it could look more like Object.tryKeys(%, 'key1', 'key2', …, 'keyN'), except that would require utilizing the pipeline syntax again, which could get confusing with the visual clutter.

tryKeys isn’t a bad alternative, but I’m weary of inflating the prototype.

I still find the proposed optional sibling syntax preferable though, as a new syntax here that builds optional chaining could be shorter, more intuitive, and more inline with the current methods for specifying object paths. It’s arguably simpler to write quickly while defining paths and could be a welcome addition to optional chaining.

Edit proposed syntax (p1?|.p2 vs. p1|.p2)

Another edit to consider here to make it clearer that this is akin to optional chaining would be to put the pipe after the question mark instead of on its own— obj?.prop1?|.prop2?.subprop ?? false would Return true for this object:

let obj = {
  prop2: {
    subprop: true,
  },
};

Using ?|. instead of |. may make it clearer that this is related/similar to optional chaining, while the pipe char indicates to move adjacently vs. deeper.

If we had gone a route like tryKeys for optional chaining, we might be using obj.tryKey(obj, 'key') instead of the optional chaining syntax, and yet the optional syntax is one of the most praised and welcomed features of JS in the last ~5 years.

Just thinking out loud here. Thanks for thinking through this with me. :slightly_smiling_face:

I spotted another use-case(-ish) example for this in the wild today which I thought I'd share here. In this comment on GitHub, the following snippet was shared:

if (pkg.svelte || pkg.module || pkg.main) {

If the intention behind this snippet was actually to do sibling nullish checks, like this:

if (pkg.svelte ?? pkg.module ?? pkg.main) {

then this could be shortened to the following with sibling-level optional chaining:

if (pkg.svelte|.module|.main) {

Similar example

The beauty behind this really shows when the beginning part of the path is longer and/or has more parts, like this:

Before:

With long explicit nullish-coalescing checks:

const friendlyName = (
  response?.data?.user.firstName ??
  response?.data?.user.nickName ??
  response?.data?.user.fullName
);
const greeting = `Howdy there, ${friendlyName ?? 'partner'}!`;

With helper variable and shorter nullish-coalescing checks:

const { firstName, nickName, fullName } = response?.data?.user;
const friendlyName = firstName ?? nickName ?? fullName;
const greeting = `Howdy there, ${friendlyName ?? 'partner'}!`;

After:

With sibling-level optional chaining:

const friendlyName = response?.data?.user.firstName|.nickName|.fullName;
const greeting = `Howdy there, ${friendlyName ?? 'partner'}!`;

@theScottyJam I ran into another simpler one today.

The ID is used to fetch the label on an object, but if the label property is not set, we fallback to the ID.

This is our current implementation:

const id: config.label ?? config.id;

This certainly isn't a bad case either, and I don't mind keeping as is, but shortening it to this could be nice:

const id: config.label|.id;

Also, I wanted to bike-shed on this hypothetical syntax a bit. I don't think either of these are currently valid syntax AFAIK, so either could be candidates for this, but I'm not sure which looks better?:

  • config.label|.id

    This first syntax swaps the usual ? from optional chaining to the pipe | to represent sibling and or-level logic and keeps the dot to represent

  • config.label?|id

    This second syntax still communicates the same while also communicating the nullish check using the ? while still preserving the pipe |.

Looks wise, alone, I think the first looks "better", but I can't tell which would even be clearer. The first seems clearer to me that it's or-ing different properties, e.g. .this|.or|.that. But I wonder if the second syntax might be clearer to some folks since the ? commonly indicates nullish checks.

I'd like to take this one step further, actually, and expand this feature from "sibling-level optional chaining" to "sibling-level chaining", because that solves a major use case that I have been wanting from basically every C-like language: caller-directed method chaining (aka "fluent API"). The "sibling chain" operator operates just like your "sibling optional chain" operator, only it operates regardless of nullity of the value: it always discards the result, then backtracks and goes to the sibling, and then it evaluates to the sibling. (This is similar to how the , operator evaluates the first argument, then discards it and evaluates the second.) If we take |. as the sibling-chain operator, we get to do something like this:

console.log("First message")
      |.error("And an error")
      |.dir(theObjectThatErrored);

and we no longer need to wonder whether the console.log() method returns this. (It doesn't.) And while that example is pretty trivial, it becomes less so when the expression on the left side has side effects:

getOrCreateConsoleForLogging().error("Something went wrong! Stack trace:")
                             |.trace();

Or as part of an optional chaining sequence:

schroedingersConsole?.log("This might not be printed")
                    |.log("But if it is this will be too");

And at that point, the question about the optional sibling-chain operator has an easy answer, because the semantics becomes "if this doesn't exist, chain to my sibling":

config.getParameterizedLabel(params)
    ?|.simpleLabel
    ?|.id;

I definitely suggest keeping the . in the syntax, though, for the same reason optional-chaining uses it: both ? and | are infix operators, and a . followed by an identifier is never syntactically valid at the start of an expression, which gives the parser a much easier time.

I don't know if this is a compelling enough use case for this to get traction, but I'd definitely be happy for it as a feature!

There is prior art for that broader idea.

Dart has Cascade Notation

An example from their webpage:

querySelector('#confirm') // Get an object.
  ?..text = 'Confirm' // Use its members.
  ..classes.add('important')
  ..onClick.listen((e) => window.alert('Confirmed!'))
  ..scrollIntoView();

Oh wow, that's neat! I haven't used Dart myself, so I didn't know about that.

I'd thought of suggesting the .. syntax as well to mimic the directory hierarchy "operator", but when I tried writing sample code with it, it got very confusing - in particular, given that so many JS libraries and interfaces do use chaining, there will always be code that uses

obj.field.methodCall()
         .nextCall()
         .anotherCall();

for chaining. And since the .nextCall() syntax seems to operate on obj.field (which it will, iff the methodCall() method returns this), it feels like using .. should allow me to access obj, but of course it doesn't:

obj.field.methodCall()
         .nextCall()
  ..anotherField.methodCall(); // Error! the anotherField prop is sought on the
                               // return value from obj.field.methodCall(),
                               // not on obj (or even necessarily on obj.field).

With the |. and ?|. syntaxes, it at least looks like something weird is happening, so I'd use a bit more caution when reading, interpreting, and writing the code.

I've never used Dart either :) - I just like reading through the documentation of different languages.

Using ".." is also a bit weird in JavaScript, because this is already valid syntax:

2..toString();

It's the number "2." (i.e. "2.0") followed by ".toString()".

Though, I guess that wouldn't be a show-stopper - I think the most consistent thing would be to use a triple dot there if you were wanting to use ".."

2...toString()

i.e. "2." followed by "..toString()".

But it's funny :).