Allow Optional Chaining w/ Destructuring

I was wondering why optional chaining cannot be used w/ destructuring.

Instead of this...

const x = foo.point?.x;
const y = foo.point?.y;
const z = foo.point?.z;

Why not...

const { ?.x, ?.y, ?.z } = point;

Note: I used point for simplicity, but the variable on the right could be nested or destructured and thus could be undefined.

Possible Extensions

const ?.{ x, y, z } = point; // When all props of an object are optional
const { coordinates: { ?.x, ?.y, ?.z } } = point; // Nested
const { coordinates: ?.{ x, y, z } } = point; // Nested All

Want only certain props of an object to be required? Hmm... perhaps we can reuse my required argument syntax.

const ?.{ x, y, z, direction! } = point; // Same for Nested

Thanks, any thoughts?

3 Likes

I'm not a fan of too much nested destructuring. It obfuscates the variable names you're declaring.

const { x, y, z } = point.coordinates ?? {};
2 Likes

This desired behavior is possible today, even with the nested syntax form (unless you're also wanting to deal with null properties):

const point = undefined
const { coordinates: { x, y, z } = {} } = point ?? {};
1 Like

If I guessed, I'd say the reason is because the meaning causes problems.

First, the optional chaining equivalent of

const x = foo.point?.x;
const y = foo.point?.y;
const z = foo.point?.z;

would more than likely look like

const { x, y, z } = foo.point?;

Neither this syntax, nor the one offered before

const { ?.x, ?.y, ?.z } = foo.point;

can work imo. The one I offered is not the optional chaining operator. Even if it were treated as such, the meaning would be

const { x, y, z } = foo.point || null; //Might be undefined. Don't recall.

which doesn't work, while the other would be problematic because the first term is a binary operator. That would break destructuring which looks for a valid identifier name as the first term after the braces.

That's not to say it can't be made to work, but it does toy around with the mental model a bit much.

According to what I know, compilers think that this is a ternary expression and complain. That's why optional method calling looks like foo.bar?.() and not foo.bar?().

The reason I wrote it that way is because of the semantic meaning of the notations involved. That's where the problems arise.

This is optional chaining.
< object being checked for existence > '?.' < property being accessed >

This is destructuring.
< declarator > '{' || '[' < property being accessed > <[ ':' < identifier > ]> <[ ',' ... ]> '}' || ']' '=' < object >

The thing to the left of the destructuring operator ('?.') must be an object for the same reasons as the '.' operator. Since the '.' is removed when using destructuring, it made sense to me that the same should happen if optionally destructuring.

However, this shows the problem fairly clearly. With normal optional chaining, the engine is either returning the desired property or an "empty" value. Distributing optional chaining over destructuring would mean this:

const { x, y, z } = null

is the same as :

const x = null;
const y = null;
const z = null;

which is distinctly not true and would require a modification of how destructuring works.

1 Like

Perhaps another angle to think of the issue, what does this do?

const point = undefined
const { ?.x, y } = point

It'll throw, because a property y does not exist on undefined. There's no value in applying this "?." syntax to only some of those properties, it's an all or nothing deal. Perhaps that's why @rdking is saying it might be more logical to put the syntax in a location where it can be an all or nothing deal.

const point = undefined
const { x, y } = point?.

Then, in his version, he was merely omitting the "." from "?.", because the property access is already implied when destructuring. It's not like const { x, y } = point. is valid syntax. He was then mentioning some issues with trying to figure out what point? would even evaluate to make this all work`. Perhaps a better syntax to achieve this effect would be something like this:

const ?{ x, y: ?{ a, b } } = point

i.e. I'm prefixing the whole destructuring object syntax with a "?" if I'm ok with it being optional.

Though once again, this syntax is mostly useless unless you're dealing with nested destructuring that has null values, and you don't wish to break up the nested destructuring. I don't know if you have use cases that fall into this category or not, but I'm certainly ok with breaking up a destructuring over multiple lines if I'm expecting a null value.

2 Likes

That's a good portion of it. Since I learned assembly as my second programming language, and C++ as my third, I've always understood operators to be implicit functions. So

const { x, y } = point?

looks to me like

function dereference(object, param) {
   let retval = object;
   if (object && typeof(object) == "object")
      retval = object[param];
   return retval;
}
const x = dereference(point, "x");
const y = dereference(point, "y");

However, parallel to what you said, I can't make a sensible function out of putting the operator on the property. So in my head, that notation is an instant fail.

1 Like

I could see that syntax being extremely convenient with optional options parameters, replacing the {foo, bar} = {} idiom with something that no longer requires an object prototype lookup when missing (making it much faster).

1 Like

How about below:

const { a?: { b?: { c } } } = obj
1 Like

That would work ok, but we would also need a syntax that works with array destructuring, and there really isn't a way to translate that to arrays.

2 Likes

I do see that as an acceptable compromise, though - I don't see myself doing optional destructuring of inner arrays that often, and it's pretty easy to just write another line to continue the destructuring.

That doesn't solve the OP, you'd need to put obj ?? {} on the right-hand-side.
Plus it's 1. drowning the name of the variable "c" in ascii soup; and 2. moving the key "a" further away from the obj you're pulling it from.

This is so much clearer:

const c = obj.a?.b?.c;
// or
const { c } = obj.a?.b ?? {};

Even though there's still a lot of soup to swim through, at least it reads left-to-right.
If someone can come up with a clean way to get rid of the ?? {}, that'd be great.

I get that when you want to pull things from multiple sub-objects, this one line can't do that. But if you have several nested optional objects you want to pull things from, perhaps you should split that into multiple steps — destructure one optional object at a time — for the sake of readability.

Now that I'm thinking about it a bit better, something that works with arguments would be far more useful (and would make the majority of use cases IMHO - think: object-based named parameters), and of course arrays would be naturally included in that.

What about this as assignment patterns as an alternative?

  • ?{a} = bif (b != null) {a} = b; else a = undefined
  • ?[a] = bif (b != null) [a] = b; else a = undefined

What about a new operator, like ?=, specifically for optional destructuring:

const obj = undefined
const { name } ?= obj

OP's destructuring would look like

const { x, y, z } ?= foo.point;

You’d want to do it per-property, I’d think, for nested destructuring as well.