For further information, see the repo on GitHub, or take a look at some examples of Turboprop in actionClick 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:
=
)
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 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
andsetting
properties on a particular object, via some kind of switch
Comments and suggestions welcome! 😀
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',
…
Top comments (5)
This is awesome. Love the thinking outside of box!
Quick comment, my expectation of this code was different. Instead of
I thought I'd get
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.
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
The library itself allows you to set it up how you like though
Yeah, this is awesome! :D
Genius stuff!!