• ES

JAVASCRIPT - OBJECTS WITH DYNAMIC KEYS

WHEN THE OBJECT PROPERTY COMES IN A VARIABLE

JAVASCRIPT - OBJECTS WITH DYNAMIC KEYS

When you don't know how it's done, it looks complicated, and when you see it done right, you wonder why it never occurred to you. It's such a simple trick that it's easy to forget if you don't use it often. In my case I used it a lot when I worked on the front end, mostly with forms.

The problem is this: you have a variable holding a name, and you want it to be the key of an object. Something like:

const fieldName = 'email';
const obj = { fieldName: 'user@example.com' };

console.log(obj); // { fieldName: 'user@example.com' }
console.log(obj[fieldName]); // undefined

See? Written like that, you end up with an object with the literal key 'fieldName', not the key 'email' you wanted. And then when you access it with obj[fieldName], it finds nothing. It's a silly mistake, and the sneaky part is that the code doesn't fail: it just doesn't do what you expected.

The solution: brackets in the definition

The answer is to wrap the variable name in brackets right in the object definition. That's called computed property names:

const fieldName = 'email';
const obj = { [fieldName]: 'user@example.com' };

console.log(obj); // { email: 'user@example.com' }
console.log(obj[fieldName]); // user@example.com
console.log(obj.email); // user@example.com

There it is. The brackets tell JavaScript "take the value of the variable, not the literal string".

It works with expressions too

It doesn't have to be just a variable. It can be any expression that returns a string (or something convertible to a string, like a number):

const prefix = 'user_';
const id = 123;

const obj = {
    [prefix + id]: 'Jaime',
    [`generated_${id}`]: true
};

console.log(obj.user_123); // Jaime
console.log(obj['generated_123']); // true

A Symbol works as a key too, but watch out: those keys don't show up in Object.keys() or JSON.stringify(), so don't use them for data you want to iterate over or serialize.

A real use case: forms

Where I used it most was in forms. You have a single handler for all the fields, and the name of the field that changed arrives in a variable. An example with React:

const [form, setForm] = useState({ name: '', email: '' });

const handleChange = (e) => {
    // e.target.name is 'name' or 'email', depending on which input changed
    setForm({ ...form, [e.target.name]: e.target.value });
};

Without the brackets you'd have to write one handler per field, or a switch with a case for each one. With them, adding a new field to the form doesn't touch the handler.

Building objects from lists

The other thing you often see is looping over data and building an object indexed by id:

const users = [
    { id: 1, name: 'Jaime' },
    { id: 2, name: 'María' },
    { id: 3, name: 'Pedro' }
];

const userById = {};
users.forEach(user => {
    userById[user.id] = user.name;
});

console.log(userById); // { '1': 'Jaime', '2': 'María', '3': 'Pedro' }

There are no brackets in the definition here, just assignment with brackets on an object that already exists. That's how it was done before ES2015, and it still works perfectly. The version with computed property names would use reduce:

const userById = users.reduce(
    (acc, user) => ({ ...acc, [user.id]: user.name }),
    {}
);

It looks compact, but it copies the whole object on every iteration, so I wouldn't use it with big lists. For that there's a more direct option, Object.fromEntries, which takes [key, value] pairs:

const userById = Object.fromEntries(
    users.map(user => [user.id, user.name])
);

And Object.defineProperty, for this?

If you ever see it used to set a key that comes in a variable, it works, but it isn't the same as assigning with brackets:

const obj = {};
const fieldName = 'email';
Object.defineProperty(obj, fieldName, {
    value: 'user@example.com',
    enumerable: true
});

obj.email = 'other@example.com';
console.log(obj.email); // user@example.com, the assignment is ignored (in strict mode it throws an error)

defineProperty is for controlling the attributes of the property, and by default writable and configurable are false. To just set a dynamic key, brackets are the direct, readable way.

One last warning

If the key comes from outside (user input, an API response), remember it can be anything, including __proto__. With brackets in the definition, as in { ...form, [e.target.name]: value }, there's no problem: __proto__ just stays as one more property of the object. The risk is in assigning with obj[key] = value, where __proto__ changes the prototype, or in functions that merge objects with keys you don't control. If you're going to pour unknown keys in like that, validate them first against a list of allowed fields.

To wrap up

It's one of those JavaScript details that seems trivial, but when you need to build objects with dynamic keys (and you need to more often than you think), it saves you from having to fiddle with strings and concatenation.


Tweet Send
0 Comments
Loading...