Even though the newer Map
class has been around for a while now, plain old javascript objects are still often being used as a map, perhaps due to the native syntax support they have, making them much less verbose to use. However, one has to be careful when using plain objects as maps as all objects will inherit properties, such as toString()
, that shouldn't be in the mapping. For this reason, it has become a common pattern to create an object without a prototype using Object.create(null)
, but even this still has a few pitfalls of its own (albeit, much more minor).
One such pitfall is that these objects do not know how to convert themselves to a primitive, so they will throw errors in scenarios such as these:
map = Object.create(null)
console.log(`Value: ${map}`) // Uncaught TypeError: Cannot convert object to primitive value
console.log(0 + map) // Uncaught TypeError: Cannot convert object to primitive value
I'm proposing that we provide a more user-friendly way to use objects as maps: Object.createMap()
- I'll refer to these kinds of maps as "Property Maps" to distinguish them from instances of the Map class.
Object.createMap()
is intended to replace Object.create(null)
usage. It will return an object with a very slim prototype (that itself inherits from null, not Object.prototype
). This prototype will never contain string keys, but it does define some symbols. For example, it can provide a definition for Symbol.toPrimitive
to allow string/number coercion to behave in a normal way:
const map = Object.createMap()
console.log('As String: ' + map) // As String: [object PropertyMap]
console.log(+map) // NaN
Object.createMap()
could conveniently provide the option to convert an existing object into a property map, by passing in that object as the first parameter.
const map = Object.createMap({ a: 1, b: 2 })
console.log(map.a) // 1
We could define Symbol.iterator
, to let one iterate over the entries of the property map.
const map = Object.createMap({ a: 1, b: 2 })
for (const [key, value] of map) {
console.log(`${key}=${value}`) // 'a=1' and 'b=2'
}
Providing Symbol.iterator
in conjunction with the iterator-helpers proposal could provide some powerful tools to lazily work with an object's entries.
The following would be an example implementation of Object.createMap()
const propMapPrototype = Object.create(null, {
[Symbol.toPrimitive]: {
enumerable: false,
value(hint) {
if (hint === 'number') return NaN
return `[object ${this[Symbol.toStringTag]()}]`
},
},
[Symbol.toStringTag]: {
enumerable: false,
value() {
return 'PropertyMap'
},
},
[Symbol.iterator]: {
enumerable: false,
*value() {
for (const key in this) {
if (Object.prototype.hasOwnProperty.call(this, key)) {
yield [key, this[key]]
}
}
},
},
})
Object.defineProperty(Object, 'createMap', {
writable: true,
configurable: true,
value(properties = {}) {
return Object.assign(Object.create(propMapPrototype), properties)
},
})