Object restructuring syntax

There seems to be a common desire for a shorthand to create an object by picking properties off of another object. Here are a few proposals other users have brought up on this forum:

  • This proposal for syntax as follows: const { a, b } as newObj = oldObj
  • This proposed object literal shortand syntax: const newObj = { oldObj.a, oldObj.b }
  • One person proposed syntax for yielding only specific properties from an object.
  • A proposed Object.pick() function.

I'd like to add to this conversation by proposing an "object restructuring" syntax, that can provide this kind of desired behavior. This syntax is inspired by the Bosque programming language's Bulk Algebraic Operations and works as follows:

const user = {
  firstNamem: 'Spider',
  lastName: 'Man',
  city: 'New York',
  street: 'Walnut Street'
}

const address = user.{ city, street, state }
console.log(address) // { city: 'New York', street: 'Walnut Street', state: undefined }

We could additionally provide syntax similar to object destructuring, to allow renaming properties and providing default values. This fits well with syntax that javascript already has, but isn't a necessary part of this proposal.

const address = user.{ city: someCity, street = 'UNKNOWN', state = 'UNKNOWN' }
console.log(nameAttributes) // { someCity: 'New York', street: 'Walnut Street', state: 'UNKNOWN' }

Some example use cases for this syntax:

const createUser = opts => Object.freeze(
  opts.{ firstName, lastName, city, state, street, zipCode }
)
async function performQuery(params) {
  const { allowEmpty = true } = params
  const query = params.{ selectAttributes: select, whereCondition: where, tableName: table }
  const paging = params.{ limit = 0, offset: start = 0 }

  const entries = await database.query({ query, paging })
  if (entries.length === 0 && !allowEmpty) throw new Error('Query Failed!')
  return entries
}

Rational behind this syntax

There are a couple of alternative ways to achieve a similar idea in the language, but they don't scale well when there's a lot of properties being extracted.

Lets take the most straight-forwards approach:

const extractQueryParams = params => ({
  select: params.select,
  where: params.where,
  table: params.table,
  allowEmpty: params.allowEmpty,
  limit: params.limit,
  start: params.offset,
  ordering: params.ordering,
  orderBy: params.orderBy,
})

Did you notice that one of those parameters got renamed in this function? Its difficult to tell. Someone who wants to make it abundantly clear which properties are getting renamed, if any, might choose to destructure, then recreate the object from locals, like in this example:

async function performAnotherQuery(params) {
  const { select, where, table, allowEmpty, limit, offset, ordering, orderBy } = params
  const queryOptions = { select, where, table, limit: start, offset, ordering, orderBy }

  const entries = await database.query(queryOptions)
  if (entries.length === 0 && !allowEmpty) throw new Error('Query Failed!')
  return entries
}

However, this approach has its own issues. Can you spot which destructured properties are not being used to create the new object? It's also pretty gross that we're polluting this function with so many local variables.

As shown before, this kind of feature request seems to keep cropping up with different syntaxes or styles, so I hope one of these feature requests gains some traction. I just thought I would throw another syntax into the conversation that I find to be intuitive, and wanted to know what other people's thoughts were on the matter.

4 Likes

Thanks for taking another hit at this! Personally I'm a fan of Ron Buckton's property shorthands based on object literals - they have the advantage of easily merging multiple objects and allowing to take in other properties as well.

The problem I see with user.{city: someCity = 'n.a.'} is that it's hard to tell (at least, not immediately obvious) whether it means {city: user.someCity ?? 'n.a.'} or {someCity: user.city ?? 'n.a.'}. The syntax looks a bit like destructuring, but without a clear target.

What do you think about {...user.{city: someCity = 'n.a.'}}, i.e. combining this with spread syntax in an object literal?

1 Like

I wasn't familiar with the Bosque programming language and I like the syntax it offers to that use-case.
I'll mention that the as in my proposal might conflict with typescript type assertion

1 Like

I do like Ron Buckton's syntax too, so I would be very happy if that were to get in instead. And you're right, it does have the advantage of being able to merge properties from different objects. For comparison, that kind of behavior would look like the bellow snippet whith the object restructuring syntax:

const newObj = {
  ...obj1.{ a, b, c },
  ...obj2.{ d, e },
  f: 2,
}

which looks pretty good too :), and combines the object restructuring syntax with the spread syntax, like you mentioned.

Your concern with user.{ city: someCity } and knowing if we're renaming city to someCity or vice verca is a valid concern. I will point out though that when I first learned object destructuring, I struggled a bit with the same issue. In the snippet const { city: someCity } = user - it's not obvious weather we're extracting city or someCity from user (except for those who have become familiar with the syntax).

I really like the syntax of this proposal! I was curious about extending it to what the as proposal is capable of, so here is what i came with:

Function parameters destructuring

const obj = { a: 1, b: 2 };

function doSomething (obj.{ a, b: newB, c = 3 }) {
  console.log(a); // 1
  console.log(newB); // 2
  console.log(c); // 3
  console.log(obj); // { a: 1, newB: 2, c: 3 }
}
doSomething(obj);

Using with imports

import obj.{ a, b as newB } from './obj.js';

console.log(a); // 1
console.log(newB); // 2
console.log(obj); // { a: 1, newB: 2 }

Destructuring assignement

const obj = { a: 1, b: 2, c: 3 };

const newObj.{a, b: newB} = obj;

console.log(a); // 1
console.log(newB); // 2
console.log(newObj); // { a: 1, newB: 2 }

pros:

  • object name placed before its properties (compared to the as syntax)
  • no conflict with Typescript as operator

cons:

  • It might not be clear at first glance what we're doing with this syntax?
    What do you think?

Using this syntax as a replacement for "as" is an interesting idea - I think you hit the pros and cons on the head. Once you're used to the syntax, it's easier to see at a glance what's going on with the object is placed before its properties instead of afterward.

Just like how { a, b, c } can either create an object literal or destructure values depending on where it's found, so to can the obj.{ a, b, c } syntax can either restructure an object or destructure, depending on where it's used.

One iffy detail is that we would probably want to support destructuring array literals with this syntax too. myArray.[a, b, c] = someArray. Even though the bosque language supported array restructuring, I purposely did not want to also support it in this proposal, as it felt pretty useless - it was basically used to just reorder elements. i.e. array = ['a', 'b', 'c']; array.[2, 0] === ['c', 'a']

I do, think these ideas are different enough that they can be different threads (or we could suggest this kind of destructuring syntax on the "as" proposal repo). They're really only related by syntax, not by functionality, so they're independent enough that one can go into the language without the other one or vice versa.

1 Like

You're right, I'm going to create a dedicated proposal to discuss about this idea :+1:

Done: New syntax for the "as" proposal

I just learned that a similar proposal had been given in the past. It seems like discussion around it eventually fizzled out, but here's the github repo. The "restructuring syntax" is almost exactly the same as what I originally proposed. They additionally allowed using it in assignment to selectively move properties from one object to another. I would advocate against that part, as it's easy enough to do the same thing using other syntax that's already in the language.

One thread it pointed to explained this syntax as "deconstructing objects into objects, rather than variables", which I think is a good way to express this syntax's objective. This explanation helps explain why when doing obj.{ a: b }, it's picking "a" from obj and assigning it to "b" in the new object (and not the other way around).