cft

Turboprop: Use JS Arrays as Property Accessors

Have you ever wished it was possible in Javascript to get or set multiple values in an object or array - all at the same time? This library allows you to do exactly that.


user

Jon Randy

a year ago | 6 min read

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, or take a look towards the end of the post for some examples of Turboprop in action.


Background

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

After writing Metho, 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:

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, 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:

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 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 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:

// 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 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:

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.

Examples of Turboprop in Action

All examples shown are with Turboprop turned on globally.

// 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 (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 Turboprop 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! 😀

Upvote


user
Created by

Jon Randy

🤖 Coding with varying degrees of success since 1983


people
Post

Upvote

Downvote

Comment

Bookmark

Share


Related Articles