JS computed property names
By Christophe Porteneuve • Published on 23 December 2021
• 3 min
Cette page est également disponible en français.
Welcome to the second installment of our Idiomatic JS series.
Object literals are neat, but property names are usually hardcoded: should you need to slap in a property (or method) whose name varies depending on the context, you used to be out of luck.
// ⛔ OLD SKOOL
function registerPlayer(name, avatar, markerPropName) {
const result = {
name: name,
avatar: avatar,
}
// The `markerPropName` argument holds a specific property name,
// expected by a third-party system, that your object must expose
// to be compatible with it. You have no idea what it'll be, so
// you must use it to dynamically name the property, like so:
result[markerPropName] = 42
return result
}
// Example call:
registerPlayer('John', 'regular-dude', '__jx87pm_player__')
// => { name: 'John', avatar: 'regular-dude', __jx87pm_player__: 42 }
But ES2015 put an end to this misery!
But first, we could start by making that example a bit nicer using shorthand properties:
const result = { name, avatar }
Now, the only reason we had to use a temporary local identifier was that we could then use JS’ indirect indexing operator […]
to dynamically access a property of that object. This is because that operator accepts any expression as its operand, then looks up the property whose name matches the result of that expression.
ES2015 introduced computed property names, which let us use that same square bracket-based syntax directly inside object literals. Check this out:
// ✅ FRESH AF
function registerPlayer(name, avatar, markerPropName) {
return { name, avatar, [markerPropName]: 42 }
}
Isn’t that SUPERNEAT™ 😍, folks?!
Methods too
This also works for shorthand methods, by the way. Say the marker property actually needs to be a method that would return some context-based stuff:
function registerPlayer(name, avatar, markerMethodName) {
return {
avatar,
name,
[markerMethodName]() {
const base = [Date.now().toString(16), avatar, name].join('-')
return base.toLowerCase().replace(/[^\w\d-]+/g, '')
},
}
}
registerPlayer('John', 'regular-dude', 'ident').ident()
// => '17d90262c01-regular-dude-john'
Symbols (especially well-known symbols)
This is actually why that syntax made it into the language: to allow our objects and classes to implement well-known symbols.
OK, so far we’ve seen th syntax and its basic usage. The remainder of this article wades into more advanced use cases, if you like to deep dive into the language. We’ll talk about other facets of JavaScript that some deem exotic, so if you’re happy with what we covered so far, feel free to stop now. We’re just showcasing some concrete examples of interplay with other facets of JS.
Circling back to symbols, let’s take iterability:
function makeDictionary(text) {
const allLetters = text.toUpperCase().replace(/\P{Letter}+/gu, '')
const uniqueLetters = new Set(Array.from(allLetters).sort())
return {
text,
// Bam! Well-known symbol!
[Symbol.iterator]() {
return uniqueLetters.values()
},
}
}
// This makes our result iterable in the JS sense, so compatible with
// lots of possible scenarios and syntaxes, such as the for-of loop:
for (const letter of makeDictionary('Awesome, darling, this rocks supreme!')) {
// letter will consecutively be 'A', 'C', 'D', 'E', 'G', 'H', 'I', 'K', 'L'…
}
Usable everywhere
As is often the case with JS, we tried to design something that would play well with other syntaxes, including shorthand methods, the async
qualifier and generative methods. This can become hairy!
function makeMetronome(periodMS = 1000) {
return {
// 😱 🤪 -- Premium Word Square! 😎
async *[Symbol.asyncIterator]() {
while (true) {
// ZOMG! await! yield!
await new Promise((res) => setTimeout(res, periodMS))
yield Date.now()
}
},
}
}
// And off you go to a suspending loop that wakes up every second with
// and up-to-date timestamp.
for await (const stamp of makeMetronome()) {
console.log('TICK!', stamp)
}
🤒 Yeah, so, if you didn’t quite grasp that one, this is perfectly normal. It is chock-full of modern syntaxes and slightly quirky notions. But if you’d like to dig in, we have a kickass training course.
One last example
This can be useful on many other occasions. Perhaps in combination with “object spread” (officially named “Rest/Spread Properties,” introduced in ES2018 and covered very soon), where this provides for cheap immutability when deriving objects dynamically, as in a hand-written, vanilla Redux reducer.
Say you have an application state slice that acts like a dictionary of progresses on goals, with keys being goal IDs and values being your progress level; something like this:
{
// 'Goal-ID': matchingProgress,
'61aa4cb905b9bed9d4814d5e': 2,
'61aa4cb9f0ef68f6c4a3b2b2': 3,
'61aa4cb94e60dbd75cff8a8d': 4,
}
Now let’s say your reducer has a PROGRESS_ON_GOAL
action with a goal ID and progress increment; you need to be able to derive a new state that leaves other progresses untouched, but does alter that specific goal’s progress. This is a key of your state object you can’t know ahead of time, it could be anything. Enter computed property names!
function goals(state, action) {
switch (action.type) {
case PROGRESS_ON_GOAL:
const { goalId, increment } = action.payload
const previous = state[goalId] || 0
// 👇 Here goes the magic! 👇
return { ...state, [goalId]: previous + increment }
default:
return state
}
}
That loocks slick… (You’ll be forgiven if you’d rather use Immer, by the way.)
There’s always more!
Besides our wealth of other articles, you may wish to look at our live training courses! In particular, if you like super-deep dives into JavaScript itself, we heartily recommend our 360° ES course!