const b = a.{ id, name, age } - Object Cherry Pick

Initial data:

const a = { id: 42, name: 'a', age: 21, score: { c:1, php: 3, js: 5 }, likes: ['X', 'Y'] };

Imagine now you want to only pick { id, name, age }.

--

:no_entry_sign: Traditional: Redundant props and object name

const b = { id: a.id, name: a.name, age: a.age }; 

--

:no_entry_sign: Two-lines Destructuring: Pollutes namespace with props as variables names

const { id, name, age } = a;
const b = { id, name, age };

--

:no_entry_sign: One-line Destructuring: Doesn't work, you get all properties of a (with score)

const b = { id, name, age } = a;

--

:white_check_mark: Object Cherry Pick

const b = a.{ id, name, age };

--

:jack_o_lantern: Bonus :

const b = a.{ id, name, age: 22 }; // Force value
const b = a.{ id, name, age ??: 22 }; // Default value ??:
const b = a.{ id, name, age as years }; // Rename
const b = a.{ id, name, ['age'] }; // Dynamic string key
const b = a.{ id, name, [/^age/] }; // Dynamic regexp keys (multiple)
const b = a.{ !id, !name }; // All props except id and name
const b = a.{ !id, !name, age:22 as years }; // Idem + Force value + Rename
const b = a.{ id, name, score.{ c, php } }; // Nested pick (Object)
const b = a.{ id, name, likes.[ 0, 1 ] }; // Nested pick (Array)
9 Likes

A notable difference between this and Object.pick method is that:

// when
assert !Object.hasOwn(a, 'name')
const picked = Object.pick(a, 'name')
const b = a.{ name }
// pick shouldn't return missing properties
assert !Object.hasOwn(picked, 'name')
// whereas .{ syntax } form should
assert Object.hasOwn(b, 'name')

What I mean by that is that these could co-exist because each fits a slightly different use-case.

I'd borrow syntax from destructuring instead of object literal + more new syntax.

const b = a.{ id, name, age = 22 }; // Default value (only when undefined)
const b = a.{ id, name, age: years }; // Rename

You'd lose the other 3 shortcuts, but I think that's not a huge loss:

const b = a.{ id, name };
b.age = 22; // Force value
b[key] = a[key]; // Dynamic string key

As for the last shortcut Β­β€” keys matching regexp β€” has imo very little chance of getting in.

  • it's another completely new syntax
  • afaik in current JS, /regex/ is always equivalent to new RegExp(/regex/.source),
    but here you'd have to essentially allow any object with Symbol.match to be used
    as matcher in brackets to maintain this equivalence
  • you'd be testing all keys in the prototype chain.
2 Likes

Would nested picking work?

const a = { w: { x: 1, y: 0 }, z: 0 };
const b = //  pick { w: { x: 1 }}

It reminded me it would be cool to exclude properties.

I added examples of exclusion in the proposal. :grinning:

const b = a.{ !id, !name }; // All props except id and name
const b = a.{ !id, !name, age:22 as years }; // Idem + Force value + Rename

I explain the choices made in the proposal :

Use ":" to affect values

: is the affectation operator for objects properties
= is the affectation operator for parameters inside functions

Since we are not inside a function, I think : should be used to affect values.

Use "as" to rename

as is the rename operator for javascript modules.
It is also a well-known rename operator in MySQL.
Following this rules, I think as should be used to rename properties.

Yep, these are just shortcuts for convenience.

Yes, that one matches multiple keys, so it can be complex to implement and/or combine with the others. I just wanted to throw the possibility without any plans behind, just to see if someone can solve this crazyness.

Talking about crazyness...

const a = { w: { x: 1, y: 0 }, z: 0 };
const b = a.{ w.{ x } };

b value is now equals to { w: { x: 1 } }

So yes, if the expression a.{ ... } is allowed to run inside objects, it works.

I added examples of nested pick in the proposal :grinning:

const b = a.{ id, name, score.{ c, php } }; // Nested pick (Object)
const b = a.{ id, name, likes.[ 0, 1 ] }; // Nested pick (Array)
2 Likes

What I meant was this:

// destructuring with default:
const { id, name, age = 22 } = a;
const b = { id, name, age };
// equiv non-polluting:
const b = a.{ id, name, age = 22 };
// destructuring with rename:
const { id, name, age: years } = a;
const b = { id, name, years };
// equiv non-polluting:
const b = a.{ id, name, age: years };
1 Like

Hello @DenisTRUFFAUT,

I think this has been proposed by @theScottyJam here.

EDIT: We already use : instead of as for renaming.

There is also an inline, rather verbose way to do this using IIFE, destructuring + construction.

const b = (({ id, name, age }) => { id, name, age })(a);

This would be slightly better if your object names are longer than a and b.

Thank you :grinning:.

1 Like

When you say already, you mean it is current valid javascript ?

Because to me the syntax a.{ age: years } just feels we are using the content of the variable years to fill the age prop. Which obviously is not a renaming operation, but an affectation. And I think the possibility use a variable as Force-value must exist. Which is why to disambiguate I think another keyword is needed, the keyword as.

const b = a.{ id, name, age as years }; // Rename ("years")
// b = { id, name, years }
const years = 22;
const b = a.{ id, name, age: years }; // Force-value (22)
// b = { id, name, age: 22 }
const years = 22;
const b = a.{ id, name, age: years as years }; // Force-value (22) + Rename ("years")
// b = { id, name, years: 22 }

I understand there are 2 proposals :
A) : for object prop affectation AND as for object prop renaming (this proposal)
B) = for object prop affectation AND : for object prop renaming (other proposals)

1 Like

That's because you're reading it as object construction. I'm reading it as object destructuring, where it obviously is a renaming operation ;)

2 Likes

Yes, I'm reading it as an object creation. :+1:

Is it really destructuring, since the brackets are at the right of the equal operator ? :face_with_monocle: :thinking:

Philosophical reading problem. :nerd_face:

1 Like

The assignment is irrelevant to this discussion. The construct on the right hand side is a stand-alone expression, usable anywhere, e.g. function call: foo(a.{ id, name }).
Destructuring is also legal in function parameters.

I think the biggest issue with your interpretation is that currently in JS, { age: age } is equivalent to { age }, both in object construction and in destructuring. In your interpretation a.{ age: age } is not the same thing as a.{ age }.

2 Likes

I agree with @lightmare,

Also if we are forcing a value to a property, it looks a bit unnecessary to pick it in the first place.

We can do it this way...

const b = { ...a.{ id, name }, age: 22 };

Instead of...

const b = a.{ id, name, age: 22 };

I don't find it to be that verbose. Thanks for the response :grinning:.

2 Likes

Yes.
I see it as a left hand instruction, without operator.
I think destructuring operations are always left sided.
Because they destructure something coming from the right (the source).

I won't speak for destructing because the approach of this proposal is, as you pointed, more like object construction.

In regular JS, without any value specified, { age } indeed transforms into { age: age }, the second age being the value of the variable age. If the variable age does not exists, an error is thrown: "Uncaught ReferenceError: age is not defined"

But there, we are explicitely providing the source with the prefix "a."
It is a factorisation, like a * ( b + c ) transforms into ab + ac so we know what the source is.

In this proposal, a.{ age } transforms into { age: a.age }.

The prefix has its own behavior and sets the default value to the object property a.age.

It doesn't break anything, it is just a new syntax with new behavior.

The regular behavior (without prefix) is still valid.

Not an issue from my point of view.

Yes we can.

1 Like

In regular JS, without any value specified, { age } indeed transforms into { age: 'age' }

This is incorrect. { age }transforms into{ age: age }, ie, using the value of the age variable.

2 Likes

Fixed, thanks

Will it work in function definitions?

function Person(p.{ name, age }) {
}
(p.{ name, age } => null)(p)

If it does then we can use it for defining parameter properties!!

class Person {
    constructor(arg1, arg2, this.{ name, age, #place }) {
    
    }
}

Reminds me of Haskell's "@"

construct person@(Person a b c)

How about Object.omit?
I'd like to see a syntax similar to:

person?.{ age, dob }
or
person.{ age, dob }!
// returns everything except age & dob.
1 Like

The syntax reads properties off an object and returns a new object. It doesn’t assign to anything.

2 Likes

Instead of negating a set of properties, I prefer negating properties granularly.
It offers more possibilities and combinations.

const b = a.{ !id, !name, age: 22 as years }; 
// b = { years: 22, score, likes }

Like aclaymore said :

This is an expression, so in itself it exists.
You can call Object() without affecting it, but does it serve a purpose ?

In functions parameters, it would probably be useful only upon affectation.

I think the following examples would be valid:

// Default value for b
const f1 = async (  b = a.{ name, age }  ) => { ... } // Param
const f2 = async ({ b = a.{ name, age } }) => { ... } // Object Param
class Person {
  id; name; age; #place;
  constructor () {
    const b = this.{ name, age, #place };
  }
}

Trying to mix the keyword this and private properties is super interesting. :+1:

In order to make this work, the keyword this have to be operational for usage.
I don't know if it can be used when you are still in parameters and not yet the body.
Question applies for both methods and constructor.
Can someone answer this question ? :astonished:

(I'm more in modules and I rarely use classes)

Sorry but I didn't get what you're saying there! can you elaborate with an example?
If it's about this binding; It should be lazily bound, meaning that the properties are only bound to this after it becomes available in the scope.

I meant to pattern match over this syntax;

// currently we can do
function fx({ a, b, c }) { //...

// with this syntax
function fx(p.{ a, b, c }) {
   console.log(p.a === a) // true
   console.log(p.z) // some other prop in `p`
}

I'm curious about its implications in the pattern matching proposal.

Inside function parameters, you cannot call this syntax without an assignment (affectation).

function fx(p.{ a, b, c }) { } // Invalid
const f = (Object()) => { }    // Invalid

Uncaught SyntaxError: Invalid destructuring assignment target

But if you assign with "a = ", it is correct:

const f = (a = Object()) => { }    // Valid
function fx(a = p.{ a, b, c }) { } // Valid, if external p exists with { a,b,c }

Here is the explanation: When you are dereferencing, your reference is the object passed to the function. But when you are calling p.{ a, b, c }, you make an explicit reference to p, so it cannot be the object you passed to the function. It takes the external object p, pick some of its properties, then creates a whole new Object(...) without any link to the parameters you passed to the function. Because it is orphan, you get the error Uncaught SyntaxError: Invalid destructuring assignment target. In order to fix this, you need to link the new object created from p.{ a, b, c } to the function parameters, and the only way to do this is through an assignment, like fx( a = p.{ a, b, c } )

How come?? I mean it's just a silly pattern matching!
like (x.{ a, b, c }) => null similar to this pattern ({ a, b, c }) => null
I hope it should be available in the pattern matching proposal like;

match(x) {
    when({ status: 200, header.{foo, bar}, body, ...rest }) {
        // do something...
    }
}