Bracket notation accepting arrays

Deeply nested object data can get unreadable pretty quick. It also can be problematic if accessing data with dot or bracket notation in multiple places in your code and the shape of the data changes. Currently bracket notation accepts strings but there would be a lot of useful things you could do if it accepted arrays. Some simple examples might be:

const obj = {
    name: {
        fName: "John",
        lName: "Smith"
    }
}
obj[["name", "fName"]] = "Jane";
const path = ["name", "fName"];
obj[path] = "Jane";
obj[path] = obj[path] + "y"; // value becomes Janey
obj["name.fName".split(".")] = "Jane";

In scenarios where you have nested JSON you could more easily dynamically construct the path for accessing your data. Also, it would allow you to store deeply nested paths in variables so that you didn’t repeat the path constantly and had a single source of truth. So instead of having obj.users[0].userInfo.name.firstName throughout your code you could have the following:

const firstName = (index) => `users.${index}.userInfo.name.firstName`.split(".");

let name = obj[firstName(0)];
obj[firstName(0)] = "Jane";

Thanks for taking a look! Any opinions/feedback is appreciated!

All of those suggestions already do something - arrays get stringified when used as object keys.

The main issue here is that obj[['x', 'y']] is already valid Javascript. Anything that goes in the brackets gets stringified before a lookup, so obj[['x', 'y']] would cause the key 'x,y' to be accessed from obj.

e.g.

const obj = { 'x,y': 42 }
console.log(obj[['x','y']]) // Logs 42

This is not a common scenario, but I'm sure people out there in the wild do this, and we don't want to break their code.

Maybe a helper function would be better?

const obj = {
    name: {
        fName: "John",
        lName: "Smith"
    }
}

Object.getProperty(obj, 'name') // Returns { fName: 'John", lName: 'Smith' }
Object.getProperty(obj, 'name', 'fName') // 'Jane'
Object.getProperty(obj, ...['name', 'lName']) // 'Smith'

Totally! Yeah, I should have thought of that :sweat_smile:. You currently just get a string key of that array. I like the idea of a helper function. How would you set values using the array?

Didn't think about that one. I guess you would need a setProperty function too:

Object.setProperty(obj, 'value', 'key1', 'key2') // This one is pretty ugly
// or
Object.setProperty(obj, 'value', ['key1', 'key2'])

That all makes sense! A get and set property function seems like a solid approach. I do wonder though if there would be some option that mimics bracket notation just for simplicity's sake.

Honestly, as we go back and forth I'm starting to wonder if there is a way to achieve something similar with proxy objects? :thinking:

Even with proxies, a key will be stringified before it's passed to the handler function - which is ok if none of your keys have commas, and you want to just .split(',') the key on commas.

I will also note that utility libraries such as lodash will provide these helper functions for you. Lodash provides them as _.get() and _.set(). For example:

const obj = {
    name: {
        fName: "John",
        lName: "Smith"
    }
}

console.log(_.get(obj, ['name', 'fName'])) // John
_.set(obj, ['name', 'lName'], 'newLastName')
1 Like

You may use Proxy like this:

const obj = {
	name: {
        fName: "John",
        lName: "Smith"
	}
}

const p = new Proxy(obj, {
		get: function(target, prop, receiver){
			prop = this._obj[prop];
			if (typeof prop === "object") {
				this._obj = prop;
				return receiver;
			};
			this._obj = target;
			return prop;
		},
		set: function(target, prop, value, receiver){
			this._obj[prop] = value;
			this._obj = target;
			return value;
		},		
		_obj: obj
	}
);

console.log(p.name.fName);
console.log(p.name.lName);
p.name.address = { city: "NY" };
console.log(p.name.address.city);

But be careful that if you call any method, the "this" object may be the global object.

1 Like

Maybe I'm missing something, but how does that proxy implementation help someone to get a nested property from a list of keys?

1 Like

This ->

const obj = {
	name: {
        fName: "John",
        lName: "Smith"
	}
}

const p = new Proxy(obj, {
		get: function(target, prop, receiver){
			prop = prop.split(",");
			let n = prop.length -1;
			for (let i = 0; i < n; i++){
				target = target[prop[i]];
			};
			return target[prop[n]];
		},
		set: function(target, prop, value, receiver){
			prop = prop.split(",");
			let n = prop.length -1;
			for (let i = 0; i < n; i++){
				target = target[prop[i]];
			};
			return target[prop[n]] = value;
		} 
	}
);
console.log(p[["name","fName"]]);
console.log(p[["name","lName"]]);
p[["name","address"]] = { city: "NY" };
console.log(p["name,address,city"]);
2 Likes

I don't know why, but this made me think about your Overloadable data accessor operator proposal @theScottyJam.

With the only difference that the @[] operator could not only be used to enable overloading access on the object, but also to accept any object type as a key (in contrary to the normal [] accessor that directly calls .toString on the object it receives, so that it is always a string).

The advantage would be that any given object would stay intact inside @[] custom get / set handlers, whereas in the proxy solution, objects like ({a: 1, b:2}) would have been already stringified to "[object Object]", and so being unusable inside the proxy get / set handlers.

Also, we would explicitly indicate that we want to treat the object as is (and not use its stringified value), so it would be more understandable than using proxies under the hood.

Here is an example to show how it could work for this use case:

// Custom getter function
const getter = function (path) {
  let value = this;
  path.forEach(key => { value = value?.[key]; });
  return value;
}

// Custom setter function
const setter = function (path, value) {
  let target = this;
  let lastKey = path.at(-1);
  path.slice(0, -1).forEach(key => {
    if (typeof target[key] !== "object") target[key] = {};
    target = target[key];
  });
  target[lastKey] = value;
}


// The object, with overloaded data accessor operator
const obj = {
  // Data accessor overloads
  [Symbol.getDataValue]: getter,
  [Symbol.setDataValue]: setter,

  // Data
  name: {
    fName: "John",
    lName: "Smith",
  },

  // Just to test
  "name,fName": "test",
}


// We can now use the `@[]` operator to get / set values using an array
const path = ["name", "fName"];
obj@[path] = "Jane";
obj@[path] += "y"; // value becomes "Janey"

// Normal `[]` accessor still works as usual
obj[path] === "test"
3 Likes

That's a nice connection that I didn't even think about @Clemdz - that would be another good solution if we wanted to enable this functionality on specific objects without being too tricky. It's interesting the different ideas that come out of that overloadable data accessor proposal.

1 Like

What about spreading in bracket notation? I don't think this isn't legal anywhere now

const path = ["name", "fName"];
obj[...path] = "Jane";

though maybe like optional chaining assignment wouldn't be allowed? (Not too familiar with the restrictions around that.)

3 Likes

You may be interested in this issue I created for using spread notation like that for Records

4 Likes

The spread makes It a very intuitive syntax, and if it goes into records, then it certainly should go into normal objects too.

1 Like

Wow, wasn't getting notifications and didn't expect to see so many responses. I am aware of lodash's get and set options. I also really agree that the spread syntax is really intuitive. So many interesting and good points here!