Turboprop: JS Arrays as Property Accessors!?!
Tags: javascript, webdev, showdev, syntax
Published: 2022-05-26 08:03:13

{% spoiler **Click for spoilers** %}
**Turboprop** allows you to use arrays as property accessors to both *get* and *set* values on objects. You are free to define which objects this will work with, and how the *getting* and *setting* will behave in each case.
Standard functionality (which can be switched on globally) is provided for `String`, `Array`, and `Object` objects:
* Multiple array items or object properties can be retrieved at once by using an array of indexes/keys as the property accessor
* Multiple array items or object properties can be set at once by passing an array of keys/indexes as the property accessor, and a corresponding array of values to be set (after the `=`)
* Complex substrings can be assembled from disparate parts of a string by using an array of string indexes as the property accessor

For further information, see the [repo on GitHub](https://github.com/jonrandy/turboprop), or take a look at some [examples of **Turboprop** in action](#examples)

---
{% endspoiler %}

## Background

So, I've been hacking JS syntax again...

After writing [Metho](https://dev.to/jonrandy/introducing-metho-safely-adding-superpowers-to-js-1lj), I kept thinking about what might else might be possible using similar techniques... Could we use something other than strings or symbols to access methods on objects? Maybe we could use arrays as property accessors? What would that even mean?

## Research: Is this even possible?

Conventional wisdom says that in JS, object property keys can only be `strings` or `symbols` - so it would appear we're fresh out of luck if we want to use an array. However, let's ignore that and give it a try:
```js
const obj = { a: 66, b: 77 }
const arr = [1, 2]
console.log(obj[arr]) // undefined
```
OK, so we don't get an error - but `undefined` is returned. Not very exciting or useful, but let's dig into what is going on...

If we check the [docs on MDN for working with objects](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Working_with_Objects), we see the following:

> Please note that all keys in the square bracket notation are converted to string unless they're Symbols, since JavaScript object property names (keys) can only be strings or Symbols (at some point, private names will also be added as the class fields proposal progresses, but you won't use them with [] form). For example, in the above code, when the key obj is added to the myObj, JavaScript will call the obj.toString() method, and use this result string as the new key.

The interesting part here is that: when accessing a property, anything that is in the square brackets that isn't a `String` or `Symbol` is '*converted to a string*', so presumably with our example above, JS is looking for a property called `"1,2"` - as that is the standard `toString` conversion for an array. Let's test that assumption:
```js
const obj = { a: 66, b: 77, "1,2": 88 }
const arr = [1, 2]
console.log(obj[arr]) // 88 !!!
```
Looks like we presumed right! So, arrays *can* be used as property accessors, but they get converted to strings first... so it's really not different or interesting at all 😢

But wait...

What if when the array is 'converted to a string', we didn't return a string at all, but returned something else? Like, perhaps, a symbol that was the 'name' of a property that we create dynamically on the target object - exactly like the way [Metho](https://dev.to/jonrandy/introducing-metho-safely-adding-superpowers-to-js-1lj) works. All we need to do is modify the `toString` method on the Array to do this as required. The created property's 'getter' function could then 'know' about the array we're attempting to use as an accessor, and use it to manipulate the target object in any way we like. We can even also define a 'setter' for this property to do two different things depending on which scenario we're dealing with.

**Note** - if you are confused at this point, you may want to read or re-read the [Metho](https://dev.to/jonrandy/introducing-metho-safely-adding-superpowers-to-js-1lj) article.

🚀 Now things are getting interesting! It is **definitely** possible to use an array in this manner... but what can we do with this new ability?

## Utilising this power

What would we want 'using an array as a property accessor' to actually do? Personally, I think the logical thing is to use the array to access multiple properties of the target object - and return those properties in an array... that somehow just feels right. When doing assignment, the obvious thing to do would seem to be to assign each item of the array being 'assigned' to a corresponding index/property in the accessor. This works nicely for Objects and Arrays, but Strings... hmmm, not so sure - I think I might want a new string to be returned that is the concatenation of all the characters referenced by the indexes in the array accessor:
```js
// Getting
const arr = ['a', 'b', 'c']
const obj = {x: 3, y: 6, z: 9}
const str = "wxyz"

arr[[0, 2]]   // ['a', 'c']
obj[['y', 'z']]   // [6, 9]
str[[1, 3]]   // "xz"

// Setting
arr[[0, 2]] = ['p', 'q']  // arr is now ['p', 'b', 'q']
obj[['y', 'z']] = [2, 1]   // obj is now {x: 3, y: 2, z: 1}
// No setting for strings, since they are immutable
```

The behaviours mentioned above are all built in to `Turboprop`, but it's also possible to define your own.

## But...  don't you break `toString` on the Array?

Errr... in a word - yes. This bothered me for quite a while after writing the first test versions of the library, as the standard functionality for turning arrays into strings is pretty useful. Luckily, I found [`Symbol.toPrimitive`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/toPrimitive) which is called (if present) when JS attempts to convert an object to a primitive value. It also appears to take precedence over `toString`.

Some quick testing revealed that the `hint` passed to this method (that tells the method what type of value to return from the conversion) was only set to 'string' in the case when the object was being used as a property accessor inside square brackets. In all other cases, the `hint` was 'default'. This allowed me to leave `toString` completely untouched, and have my `toPrimitive` method only return a Symbol when necessary... leaving the default behaviour intact for all other coercion situations.

This still isn't perfect, but so far I've seen no way to improve on it.

## Turning all this into 'Turboprop'

The library abstracts away all the finicky mechanisms described above and *hopefully* provides a pretty simple interface to use the functionality in your own projects. The simplest way to use it is to simply switch it on globally, and use the default behaviours - which I think are quite useful:
```js
import * as turboprop from "turboprop"
turboprop.initialiseGlobally()
```
If you want to use the general concept of array-based property access in a different way though, that is totally possible, and is explained in the [documentation](https://github.com/jonrandy/turboprop).

## Examples of Turboprop in Action <a name="examples"></a>
All examples shown are with **Turboprop** turned on globally.
```js
// Retrieve multiple values from an array
const arr = ['a', 'b', 'c', 'd', 'e']
console.log(arr[[0, 2, 4]])   // ["a", "c", "e"]

// Nested retrieval
console.log(arr[[0, 2, [3,4]]])   // ["a", "c", ["d", "e"]]

// Setting multiple values in an array
arr[[1, 3, 5]] = ['r', 'h', 's']
console.log(arr)   // ["a", "r", "c", "h", "e", "s"]

// combine getting and setting array values
arr[[0,1]] = arr[[2,0]]
console.log(arr)   // ["c", "a", "c", "h", "e", "s"]


// Retrieve multiple object properties
const obj = {a: 5, b: 6, c: 7, d: 8, e: 9}
console.log(obj[['b', 'c', 'e']])   // [6, 7, 9]

// Set multiple object properties
obj[['a', 'e']] = [33, 66]
console.log(obj)   // { a: 33, b: 6, c: 7, d: 8, e: 66 }


// Retrieve multiple characters from a string
const str = "Hello world!"
console.log(str[[0, 4, 7, 10, 11]])   // 'Hood!'
console.log(str[0[to(3)], 6[to(10)]])   // 'Hell world' (using 'to' from metho-number)

// Strings are immutable - hence no setting of values is possible

// More useful(?) examples
const addr = "123 High Street, My Town, My State"
const address = {}
address[['line1', 'line2', 'line3']] = addr.split(',').map(s=>s.trim())
console.log(address)   // { line1: "123 High Street", line2: "My Town", line3: "My State" }

const obj1 = {type: 'box', col1: 'red', col2: 'blue', col3: 'green'}
const obj2 = {}
obj2[['item', 'colours']] = obj1[['type', ['col1', 'col2', 'col3']]
console.log(obj2)   // {item: 'box', colours:['red', 'blue', 'green']}
```
## With great power comes great responsibility...

Unlike [Metho](https://dev.to/jonrandy/introducing-metho-safely-adding-superpowers-to-js-1lj) (which is totally safe to use with other libraries) - **Turboprop** runs the potential risk of conflicts (since we make a modification to a fairly core, but hopefully rarely used part of the JS language internals). I would suggest a full test of any system before going live if you choose to use it globally.

## Final thoughts, future plans

Hopefully, you may find this library useful - or at the very least, interesting. Possible future enhancements include:
* Conflict detection (warn the user before it happens)
* Alternate functionalities for `getting` and `setting` properties on a particular object, via some kind of switch

Comments and suggestions welcome! 😀

{% embed https://github.com/jonrandy/turboprop %}