DEV Community

Cover image for Turboprop: JS Arrays as Property Accessors!?!
Jon Randy πŸŽ–οΈ
Jon Randy πŸŽ–οΈ

Posted on

Turboprop: JS Arrays as Property Accessors!?!

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, or take a look at 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
Enter fullscreen mode Exit fullscreen mode

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 !!!
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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']}
Enter fullscreen mode Exit fullscreen mode

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 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! πŸ˜€

GitHub logo jonrandy / turboprop

Use arrays as property accessors

Turboprop

Turboprop provides functionality to modify JavaScript arrays so that they can be used as property accessors (both for getting, and setting values). It also lets you define the methods by which the target object's properties are get and set using the accessor array.

That probably seems a lot like gibberish, so the best way to explain is with some sample code:

// 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',
…
Enter fullscreen mode Exit fullscreen mode

Top comments (5)

Collapse
 
robinpokorny profile image
Robin Pokorny

This is awesome. Love the thinking outside of box!

Quick comment, my expectation of this code was different. Instead of

console.log(obj[['b', 'c', 'e']])   // [6, 7, 9]
Enter fullscreen mode Exit fullscreen mode

I thought I'd get

console.log(obj[['b', 'c', 'e']])   // { b: 6, c: 7, e: 9 }
Enter fullscreen mode Exit fullscreen mode

That is more like _.pick instead of _.at (= R.props). I understand that it kind of breaks some of the combinations later.

It is possible to allow objects as properly accessors, but I think that would be cumbersome.

Collapse
 
jonrandy profile image
Jon Randy πŸŽ–οΈ

Yeah, I was torn between making the default behaviour do that, or what I ended up picking. One idea for the future is to make the behaviour switchable somehow

Collapse
 
jonrandy profile image
Jon Randy πŸŽ–οΈ

The library itself allows you to set it up how you like though

Collapse
 
christiankozalla profile image
Christian Kozalla

Yeah, this is awesome! :D

Collapse
 
miketalbot profile image
Mike Talbot ⭐

Genius stuff!!