Proposal: Object gather in parameters lists
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
LICENSE
README.md

README.md

tc39-proposal-object-gather

Proposal: Object gather in parameters lists

This is a proposal to allow gathering one ore more specified arguments into one or more objects when a function is invoked. It's already possible to gather some or all arguments into an array because there is no need to provide a key for each argument: array are objects that use numeric values as keys.
To enable sort of object gathering in parameters lists is mandatory to provide keys.

Proposal details

Object gather

This syntax uses the well know gather/rest operator ... in conjunction with the block syntax {} cointaining a key list - that acts also as parameter list - to directly create a non constant object (identifiers in a normal parameter list are never constant) that gathers all the parameters present into its list.
Destructuring is not involved.

function F(...obj{par1, par2}) {
  obj; // {par1: .., par2: ..}
  
  arguments[0] == obj.par1; // true
  arguments[1] == obj.par2; // true
}

It might be said that the object is an implicit parameter. On the other hand the key list contains all the explicit parameters that have to be passed and will populate the arguments array-like object.
That function will be called as we call a function that uses the array rest. This is one strength of the proposal because the call site remains clean:

F(arg1, arg2);


Of course if we supply less arguments than requested, corresponding props will be undefined:

function F(...obj{par1, par2, par3}) {
  obj; // {par1: .., par2: .., par3: ..}
}
F(arg1); // 'obj.par2' and 'obj.par3' will be undefined

 

Object gather + other parameters

It will be possible to not gather all parameters:

function F(par1, ...obj{par2, par3}, par4) {
  par1; // ..
  obj; // {par2: .., par3: ..}
  par4; // ..
  
  arguments[0] == par1; // true
  arguments[1] == obj.par2; // true
  arguments[2] == obj.par3; // true
  arguments[3] == par4; // true
  arguments[4] == void 0; // true
}

Here the function call:

F(arg1, arg2, arg3, arg4); 
// 'obj' will gather arg2 and arg3
// arg1and arg3 will populate normal parameters

 

Object gather + array rest

We can still use the array rest operator in multiple ways:

  1. After the object gather
function F(...obj{par1, par2}, ...array) {
  obj; // {par1: .., par2: ..}
  array; // [.., .., .., ...]
  
  arguments[0] == obj.par1; // true
  arguments[1] == obj.par2; // true
  arguments[2] == array[0]; // true
  arguments[3] == array[1]; // true
  // ...
}

Here the function call:

F(arg1, arg2, arg3, arg4, arg5); 
// 'obj' will gather arg1 and arg2
// 'array' will gather or the other parameters

But not before object gather:

function F(...array, ...obj{par1, par2}) {
    // Error: obj couldn't be populated
}
  1. Inside the object gather at the bottom of the key list
function F(...obj{par1, par2, ...array}) {
  obj; // {par1: .., par2: .., array: [.., .., .., ...]}
  
  arguments[0] == obj.par1; // true
  arguments[1] == obj.par2; // true
  arguments[2] == obj.array[0]; // true
}

Here the function call:

F(arg1, arg2, arg3, arg4, arg5); 
// 'obj' will gather arg1 and arg2 directly and all the other params into its 'array' prop

We cannot add any parameters in the function signature if we use the array rest inside object gather:

function F(...obj{par1, par2, ...array}, ...array2) {
    // Error: array2 couldn't be populated
}

 

Object gather + object gather

More than one object gather could be used toghether, mainly in two ways:

  1. Sibling objects gather
function F(...obj{par1, par2}, ...obj2{par3, par4}) {
  obj; // {par1: .., par2: ..}
  obj2; // {par3: .., par4: ..}
  
  arguments[0] == obj.par1; // true
  arguments[1] == obj.par2; // true
  arguments[2] == obj2.par3; // true
  arguments[3] == obj2.par4; // true
  arguments[4] == void 0; // true
}

Here the function call:

F(arg1, arg2, arg3, arg4); 
// 'obj' will gather arg1 and arg2
// 'obj2' will gather arg3 and arg4
  1. Nested objects gather
function F(...obj{par1, ...obj2{par2, par3}, par4}) {
  obj; // {par1: .., obj2: {par2: .., par3: ..}, par4: .. }
  
  arguments[0] == obj.par1; // true
  arguments[1] == obj.obj2.par2; // true
  arguments[2] == obj.obj2.par3; // true
  arguments[3] == obj.par4; // true
  arguments[4] == void 0; // true
}

Here the function call:

F(arg1, arg2, arg3, arg4); 
// 'obj' will gather arg1 and arg4 directly
// 'obj' will gather arg2 and arg3 into its 'obj2' object prop

 

Special case: this object gather

In constructor functions, and in the constructor method of classes, the object gather syntax could be useful to quickly assign all parameters to the this object:

function F(...this{par1, par2, par3}) {
  this; // {par1: .., par2: .., par3: ..}
  
  arguments[0] == this.par1; // true
  arguments[1] == this.par2; // true
  arguments[2] == this.par3; // true
  arguments[3] == void 0; // true
}

Here the weel know function call:

new F(arg1, arg2, arg3); 
// 'this' will gather all the arguments


We cannot use this syntax in subclasses because of the prior super() call:

constructor(...this{par1, par2, par3}) {
  //  ReferenceError: must call super constructor before using |this| in ... class constructor
}

Reasons

  • Currently this type of gather is not allowed in JavaScript.
  • It allows us to directly map function arguments to object properties in a programmed order.
  • It provides a fast and unequivocal way to construct object using parameters that could be easily optimized by the engine.
  • It provides a fast and clean way to create common constructor functions.
  • Current solutions imply one or more of the following: changes to the function signature, useless objects creations, annoying identifiers repetitions or an additional function call. Let's briefly see them:

annoying identifiers repetitions

To not change the function signature:

function F(par1, par2, par3, ..., parN){
  // function body
}

to gather arguments into an object we fall in the case of the annoying identifiers repetitions. This is not DRY:

function F(par1, par2, par3, ..., parN){
  const obj = { par1, par2, par3, ..., parN }
}

changes to the function signature

To avoid above repetitions we are forced to change the function signature:

function F(obj){
  // function body
}

calling the function in this way:

F({arg1, arg2, arg3, ..., argN});

It could seem a minor issue. But why if the object is a construct necessary only for function's internal needs have we to create it in the call site?

annoying identifiers repetitions || useless objects creations + additional function call

Them are evident when we are in a constructor function:

function constructor(par1, par2, par3, ..., parN){
  this.par1 = par1;
  this.par2 = par2;
  this.par3 = par3;
  ...
  this.parN = parN;
}

This is annoying and obviously not a DRY approach. We could have done like this:

function constructor(par1, par2, par3, ..., parN){
  Object.assign(this, {par1, par2, par3, ..., parN});
}

Here we are creating and destroying in a short time an object, copying each value two times: from the parameters to the object, then from the object to this. It is also present a function call to Object.assign.

Obviously not all the alternatives have been considered, but each of them presents one or more of the aforementioned problems.

How could the syntax be transpiled?

For the sake of argument we can assert that Babel could not follow a DRY approach transpiling our code, giving priority to a faster and less memory needy solution. Probably the solutions tagged with annoying identifiers repetitions are the best:

normal object gather

function F(...obj1{par1a, par1b}, ...obj2{par2a, par2b}) {
    // function body
}

could be transpiled into:

function F(par1a, par1b, par2a, par2b) {
    let obj1 = {par1a, par1b};
    let obj2 = {par2a, par2b};
    // function body
}

Parameters could be reassigned so we use let instead of const.

nested object gather

function F(...obj{par1, ...obj2{par2, par3}, par4}) {
    // function body
}

could be transpiled into:

function F(par1, par2, par3, par4) {
    let obj1 = {par1, obj2: {par2, par3}, par4};
    // function body
}

this gather

function F(...this{par1, par2, par3}) {
    // function body
}

could be transpiled into:

function F(par1, par2, par3) {
    this.par1 = par1;
    this.par2 = par2;
    this.par3 = par3;
}