docs/beyond/concepts/computed-property-names.mdx
Have you ever needed to create an object where the property name comes from a variable? Before ES6, this required creating the object first, then adding the property in a separate step. Computed property names changed everything.
// Before ES6 - two steps required
const key = 'status';
const obj = {};
obj[key] = 'active';
// ES6 computed property names - single expression
const key2 = 'status';
const obj2 = { [key2]: 'active' };
console.log(obj2); // { status: 'active' }
With computed property names, introduced in the ECMAScript 2015 specification, you can use any expression inside square brackets [] within an object literal, and JavaScript evaluates that expression to determine the property name. This seemingly small syntax addition enables powerful patterns for dynamic object creation.
Computed property names are an ES6 feature that allows you to use an expression inside square brackets [] within an object literal to dynamically determine a property's name at runtime. The expression is evaluated, converted to a string (or kept as a Symbol), and used as the property key. This enables creating objects with dynamic keys in a single expression, eliminating the need for the two-step create-then-assign pattern required before ES6.
const field = 'email';
const value = '[email protected]';
// The expression [field] is evaluated to get the key name
const formData = {
[field]: value,
[`${field}_verified`]: true
};
console.log(formData);
// { email: '[email protected]', email_verified: true }
Think of computed property names as dynamic labels for your object's filing cabinet. Instead of pre-printing labels (static keys), you're using a label maker (the expression) to print the label right when you create the file.
Imagine you're organizing a filing cabinet. With traditional object literals, you must know all the label names in advance. With computed properties, you can generate labels on the fly.
┌─────────────────────────────────────────────────────────────────────────────┐
│ COMPUTED PROPERTY NAMES: DYNAMIC LABELS │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ STATIC KEYS (Traditional) COMPUTED KEYS (ES6) │
│ ───────────────────────── ────────────────────── │
│ │
│ Pre-printed labels: Label maker: │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ name: "Alice" │ │ [key]: "Alice" │ │
│ │ age: 30 │ │ [prefix+id]: 30 │ │
│ └──────────────────┘ └──────────────────┘ │
│ │ │
│ You must know "name" key can be any │
│ and "age" at write time expression evaluated │
│ at runtime │
│ │
│ const obj = { const key = 'name'; │
│ name: "Alice", const obj = { │
│ age: 30 ──────────────► [key]: "Alice", │
│ }; [`user_${key}`]: "Alice" │
│ }; │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
The syntax is straightforward: wrap any expression in square brackets [] where you would normally write a property name.
The most common use case is using a variable's value as the property name:
const propName = 'score';
const player = {
name: 'Alice',
[propName]: 100
};
console.log(player); // { name: 'Alice', score: 100 }
console.log(player.score); // 100
Template literals let you build dynamic key names with string interpolation:
const prefix = 'user';
const id = 42;
const data = {
[`${prefix}_${id}`]: 'Alice',
[`${prefix}_${id}_role`]: 'admin'
};
console.log(data);
// { user_42: 'Alice', user_42_role: 'admin' }
Any valid JavaScript expression works inside the brackets:
const i = 0;
const obj = {
['prop' + (i + 1)]: 'first',
['prop' + (i + 2)]: 'second',
[1 + 1]: 'number key'
};
console.log(obj);
// { '2': 'number key', prop1: 'first', prop2: 'second' }
You can even call functions to generate key names:
function getKey(type) {
return `data_${type}_${Date.now()}`;
}
const cache = {
[getKey('user')]: { name: 'Alice' }
};
console.log(Object.keys(cache)[0]);
// Something like: 'data_user_1699123456789'
Understanding the evaluation order is crucial for avoiding subtle bugs.
When JavaScript encounters a computed property, it evaluates the key expression first, then the value expression. Properties are processed left-to-right in source order.
let counter = 0;
const obj = {
[++counter]: counter, // key: 1, value: 1
[++counter]: counter, // key: 2, value: 2
[++counter]: counter // key: 3, value: 3
};
console.log(obj);
// { '1': 1, '2': 2, '3': 3 }
Each property's key expression (++counter) is evaluated before its value expression (counter), so the key and value end up with the same number.
Property keys can only be strings or Symbols. When you use any other type, JavaScript converts it using an internal operation called ToPropertyKey():
| Input Type | Conversion |
|---|---|
| String | Used as-is |
| Symbol | Used as-is |
| Number | Converted to string: 42 → "42" |
| Boolean | true → "true", false → "false" |
| null | "null" |
| undefined | "undefined" |
| Object | Calls toString() → usually "[object Object]" |
| Array | Calls toString() → [1,2,3] becomes "1,2,3" |
const obj = {
[42]: 'number',
[true]: 'boolean',
[null]: 'null',
[[1, 2, 3]]: 'array'
};
console.log(obj);
// { '42': 'number', 'true': 'boolean', 'null': 'null', '1,2,3': 'array' }
// Number keys and string keys can collide!
console.log(obj[42]); // 'number'
console.log(obj['42']); // 'number' (same property!)
Before computed property names, creating objects with dynamic keys required multiple steps:
// ES5: Create object, then add dynamic property
function createUser(role, name) {
var obj = {};
obj[role] = name;
return obj;
}
var admin = createUser('admin', 'Alice');
console.log(admin); // { admin: 'Alice' }
This was especially awkward in situations requiring single expressions:
// ES5: IIFE pattern for single-expression dynamic keys
var role = 'admin';
var users = (function() {
var obj = {};
obj[role] = 'Alice';
return obj;
})();
// ES6: Clean single expression
const role2 = 'admin';
const users2 = { [role2]: 'Alice' };
The ES6 syntax shines in:
map() and reduce()// ES6 enables elegant patterns
const fields = ['name', 'email', 'age'];
const defaults = fields.reduce(
(acc, field) => ({ ...acc, [field]: '' }),
{}
);
console.log(defaults);
// { name: '', email: '', age: '' }
Symbols are unique, immutable identifiers that can only be used as object keys via computed property syntax. According to MDN, this is one of the most important use cases for computed properties and the reason Symbols were designed alongside this syntax in ES2015.
You cannot use a Symbol with the shorthand or colon syntax:
const mySymbol = Symbol('id');
// This creates a string key "mySymbol", NOT a Symbol key!
const wrong = { mySymbol: 'value' };
console.log(Object.keys(wrong)); // ['mySymbol']
// This uses the Symbol as the key
const correct = { [mySymbol]: 'value' };
console.log(Object.keys(correct)); // [] (Symbols don't appear in keys!)
console.log(Object.getOwnPropertySymbols(correct)); // [Symbol(id)]
Symbol-keyed properties don't appear in most iteration methods:
const secret = Symbol('secret');
const user = {
name: 'Alice',
[secret]: 'classified information'
};
// Symbol keys are hidden from these:
console.log(Object.keys(user)); // ['name']
console.log(JSON.stringify(user)); // '{"name":"Alice"}'
for (const key in user) {
console.log(key); // Only logs 'name'
}
// But you can still access them:
console.log(user[secret]); // 'classified information'
console.log(Object.getOwnPropertySymbols(user)); // [Symbol(secret)]
JavaScript has built-in "well-known" Symbols that let you customize how objects behave. These must be used with computed property syntax.
const range = {
start: 1,
end: 5,
[Symbol.iterator]() {
let current = this.start;
const end = this.end;
return {
next() {
if (current <= end) {
return { value: current++, done: false };
}
return { done: true };
}
};
}
};
console.log([...range]); // [1, 2, 3, 4, 5]
for (const num of range) {
console.log(num); // 1, 2, 3, 4, 5
}
const myCollection = {
items: [],
[Symbol.toStringTag]: 'MyCollection'
};
console.log(Object.prototype.toString.call(myCollection));
// '[object MyCollection]'
// Compare to a plain object:
console.log(Object.prototype.toString.call({}));
// '[object Object]'
const temperature = {
celsius: 20,
[Symbol.toPrimitive](hint) {
switch (hint) {
case 'number':
return this.celsius;
case 'string':
return `${this.celsius}°C`;
default:
return this.celsius;
}
}
};
console.log(+temperature); // 20 (number hint)
console.log(`${temperature}`); // '20°C' (string hint)
console.log(temperature + 10); // 30 (default hint)
While not truly private, Symbol keys provide a level of encapsulation:
// Module-scoped Symbol - not exported
const _balance = Symbol('balance');
class BankAccount {
constructor(initial) {
this[_balance] = initial;
}
deposit(amount) {
this[_balance] += amount;
}
getBalance() {
return this[_balance];
}
}
const account = new BankAccount(100);
console.log(Object.keys(account)); // []
console.log(JSON.stringify(account)); // '{}'
console.log(account.getBalance()); // 100
// Still accessible if you know about Symbols:
const symbols = Object.getOwnPropertySymbols(account);
console.log(account[symbols[0]]); // 100
Computed property syntax works with method shorthand for dynamically-named methods:
const action = 'greet';
const obj = {
[action]() {
return 'Hello!';
},
[`${action}Loudly`]() {
return 'HELLO!';
}
};
console.log(obj.greet()); // 'Hello!'
console.log(obj.greetLoudly()); // 'HELLO!'
const iteratorName = 'values';
const collection = {
items: [1, 2, 3],
*[iteratorName]() {
for (const item of this.items) {
yield item * 2;
}
}
};
console.log([...collection.values()]); // [2, 4, 6]
const fetchName = 'fetchData';
const api = {
async [fetchName](url) {
const response = await fetch(url);
return response.json();
}
};
// api.fetchData('https://api.example.com/data')
You can combine computed property names with getters and setters:
const prop = 'fullName';
const person = {
firstName: 'Alice',
lastName: 'Smith',
get [prop]() {
return `${this.firstName} ${this.lastName}`;
},
set [prop](value) {
const parts = value.split(' ');
this.firstName = parts[0];
this.lastName = parts[1];
}
};
console.log(person.fullName); // 'Alice Smith'
person.fullName = 'Bob Jones';
console.log(person.firstName); // 'Bob'
console.log(person.lastName); // 'Jones'
const _value = Symbol('value');
const validated = {
[_value]: 0,
get [Symbol.for('value')]() {
return this[_value];
},
set [Symbol.for('value')](v) {
if (typeof v !== 'number') {
throw new TypeError('Value must be a number');
}
this[_value] = v;
}
};
validated[Symbol.for('value')] = 42;
console.log(validated[Symbol.for('value')]); // 42
React and Vue state updates commonly use computed properties. According to Stack Overflow's 2023 Developer Survey, React remains the most popular front-end framework, making this pattern one of the most widely used applications of computed property names:
// React-style form handler
function handleInputChange(fieldName, value) {
return {
[fieldName]: value,
[`${fieldName}Touched`]: true,
[`${fieldName}Error`]: null
};
}
const updates = handleInputChange('email', '[email protected]');
console.log(updates);
// {
// email: '[email protected]',
// emailTouched: true,
// emailError: null
// }
// Reducer pattern with computed properties
function updateField(state, field, value) {
return {
...state,
[field]: value,
lastModified: Date.now()
};
}
const state = { name: 'Alice', email: '' };
const newState = updateField(state, 'email', '[email protected]');
console.log(newState);
// { name: 'Alice', email: '[email protected]', lastModified: 1699123456789 }
function createTranslations(locale, translations) {
return {
[`messages_${locale}`]: translations,
[`${locale}_loaded`]: true,
[`${locale}_timestamp`]: Date.now()
};
}
const spanish = createTranslations('es', { hello: 'hola' });
console.log(spanish);
// {
// messages_es: { hello: 'hola' },
// es_loaded: true,
// es_timestamp: 1699123456789
// }
function normalizeResponse(entityType, items) {
return items.reduce((acc, item) => ({
...acc,
[`${entityType}_${item.id}`]: item
}), {});
}
const users = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' }
];
const normalized = normalizeResponse('user', users);
console.log(normalized);
// {
// user_1: { id: 1, name: 'Alice' },
// user_2: { id: 2, name: 'Bob' }
// }
When multiple computed properties evaluate to the same key, the last one overwrites previous values:
const key = 'same';
const obj = {
[key]: 'first',
['sa' + 'me']: 'second',
same: 'third' // Static key, same string
};
console.log(obj); // { same: 'third' }
If the key expression throws, object creation is aborted entirely:
function badKey() {
throw new Error('Key evaluation failed');
}
// This throws before the object is created
try {
const obj = {
valid: 'ok',
[badKey()]: 'never reached'
};
} catch (e) {
console.log(e.message); // 'Key evaluation failed'
}
Objects used as keys call toString(), which can cause unexpected collisions:
const objA = { toString: () => 'key' };
const objB = { toString: () => 'key' };
const data = {
[objA]: 'first',
[objB]: 'second' // Overwrites! Both → 'key'
};
console.log(data); // { key: 'second' }
__proto__ Special CaseThe __proto__ key has special behavior depending on how it's written:
// Non-computed: Sets the prototype!
const obj1 = { __proto__: Array.prototype };
console.log(obj1 instanceof Array); // true
console.log(Object.hasOwn(obj1, '__proto__')); // false
// Computed: Creates a normal property
const obj2 = { ['__proto__']: Array.prototype };
console.log(obj2 instanceof Array); // false
console.log(Object.hasOwn(obj2, '__proto__')); // true
// Shorthand: Also creates a normal property
const __proto__ = 'just a string';
const obj3 = { __proto__ };
console.log(obj3.__proto__); // 'just a string' (own property)
Computed properties use [expression] syntax in object literals to create dynamic key names at runtime.
The key expression is evaluated before the value expression. Properties are processed left-to-right in source order.
Non-string/Symbol keys are coerced via ToPropertyKey(). Numbers become strings, objects call toString().
Symbols can ONLY be used as keys via computed property syntax. The syntax { mySymbol: value } creates a string key "mySymbol".
Well-known Symbols customize object behavior. Use [Symbol.iterator] for iteration, [Symbol.toStringTag] for type strings.
Computed method syntax enables dynamic method names. Works with regular methods, generators, and async methods.
Computed getters/setters enable dynamic accessor properties. Combine get [expr]() and set [expr](v) for dynamic accessors.
Pre-ES6 required two steps; ES6 enables single-expression objects. This is especially useful in reduce(), arrow functions, and default parameters.
Duplicate computed keys are allowed—last one wins. No error is thrown; the later value simply overwrites.
The __proto__ key behaves differently in computed vs non-computed form. Only non-computed colon syntax sets the prototype.
- `{ key: value }` creates a property with the literal name `"key"` (a static string).
- `{ [key]: value }` evaluates the variable `key` and uses its **value** as the property name.
```javascript
const key = 'dynamicName';
const static = { key: 'value' };
console.log(static); // { key: 'value' }
const dynamic = { [key]: 'value' };
console.log(dynamic); // { dynamicName: 'value' }
```
The square brackets signal "evaluate this expression to get the key name."
The **key expression is evaluated first**, then the **value expression**. This happens for each property in left-to-right order.
```javascript
let n = 0;
const obj = {
[++n]: n, // key: 1, value: 1
[++n]: n // key: 2, value: 2
};
// { '1': 1, '2': 2 }
```
The `++n` in the key runs before `n` in the value is read, so they match.
The object is converted to a string via its `toString()` method. By default, this returns `"[object Object]"`, which can cause unintended collisions:
```javascript
const a = { id: 1 };
const b = { id: 2 };
const obj = {
[a]: 'first',
[b]: 'second' // Overwrites! Both → "[object Object]"
};
console.log(obj); // { '[object Object]': 'second' }
```
Custom `toString()` methods can provide unique keys, but this pattern is error-prone. Use Symbols or string IDs instead.
The shorthand and colon syntax only accept identifiers or string literals as property names. Writing `{ mySymbol: value }` creates a property named `"mySymbol"` (a string), not a Symbol-keyed property.
```javascript
const sym = Symbol('id');
const wrong = { sym: 'value' };
console.log(Object.keys(wrong)); // ['sym'] - string key!
const right = { [sym]: 'value' };
console.log(Object.keys(right)); // [] - Symbol key is hidden
console.log(Object.getOwnPropertySymbols(right)); // [Symbol(id)]
```
The `[sym]` syntax tells JavaScript to evaluate the variable and use the Symbol itself as the key.
Use computed property syntax with method shorthand:
```javascript
const action = 'processData';
const handler = {
[action](data) {
return data.map(x => x * 2);
},
// Generator method
*[`${action}Iterator`](data) {
for (const item of data) {
yield item * 2;
}
},
// Async method
async [`${action}Async`](url) {
const response = await fetch(url);
return response.json();
}
};
console.log(handler.processData([1, 2, 3])); // [2, 4, 6]
```
This works with regular methods, generators (`*[name]()`), and async methods (`async [name]()`).
Duplicate keys are allowed—the **last one wins** and overwrites previous values. No error is thrown:
```javascript
const obj = {
['x']: 1,
['x']: 2,
x: 3
};
console.log(obj); // { x: 3 }
```
This applies whether the duplicate comes from computed properties, static properties, or a mix. The same rule applies to the rest of JavaScript—later assignments overwrite earlier ones.