Published on

Weird JS Array

Authors
  • avatar
    Name
    Cookie
    Twitter

Recently, I updated a form field in my job, changing its input type from radio to checkbox to allow the user to deselect the option. This also meant the field's value changed from a string to an array containing a single string element, for consistency with other checkbox fields.

Since our i18n system uses a key-mapping approach for translations, I expected the change to break the translation. However, I was surprised to find that it still worked correctly. Curious about what was happening during the key lookup in an object, I decided to investigate further.

My first hypothesis was that the array was implicitly converted to a string during the key lookup. To test this, I intercepted the get method using a Proxy to log any properties accessed on the array:

const array = new Proxy([], {
  get: function (target, prop, receiver) {
    console.log('Getting: ', prop)
    return Reflect.get(target, prop, receiver)
  },
})

Then, I create an object with predefined keys and attempted a lookup using the array:

const obj = {
  a: 'Hello',
  'a,b': 'Hello World.',
}

console.log(obj[array])

// Console output:
// Getting: , Symbol(Symbol.toPrimitive)
// Getting: , toString
// Getting: , join
// Getting: , length
// undefined

As shown, the toString() method is indeed called, confirming my initial assumption. To further verify, I added two elements to the array:

array.push('a')
console.log(obj[array]) // 'Hello'

array.push('b')
console.log(obj[array]) // 'Hello World.

When using non-string types as object keys, JavaScript implicitly converts them to string.1 Therefore, you can use anything to index an object in JavaScript as long as it has a toString method.

obj[true]
function foo() {}
obj[foo]
const sym = Symbol('foo')
obj[sym]

All primitive types, except null and undefined, have their corresponding object wrappers. When a property is accessed on a primitive value, JavaScript automatically wraps the value into the corresponding wrapper object and accesses the toString property on the object instead.2

In the console output we also see Symbol(Symbol.toPrimitive). This method allows objects to specify the returned primitive representation based on a provided hint(an argument indicating the preferred type), and it's called before falling back to the toString method.3

Footnotes

  1. JavaScript data types and data structures: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#properties

  2. JavaScript data types and data structures: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#primitive_values

  3. Symbol.toPrimitive: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/toPrimitive