DEV Community

Cover image for Future Javascript: Records and Tuples
Johnny Simpson
Johnny Simpson

Posted on • Originally published at fjolt.com

Future Javascript: Records and Tuples

Records and Tuples are an upcoming feature for Javascript, which may be familiar if you have used other languages. They are very similar to Arrays and Objects, the key difference being that they are immutable, meaning they can't be updated or changed. They give us a brand new primitive type in Javascript, and let us do a lot of things we couldn't previously do, including comparing objects and arrays by value, rather than identity. In this article, we'll cover how they work, and how you can use them today.

Support for Records and Tuples

Currently, records and tuples are a stage 2 proposal for Javascript. Broadly speaking, this means that they are relatively stable, but not implemented as a standard spec. As such, major browsers and backend Javascript like Node.JS do not implement them. You can, however, use them, if you have Babel, by using this polyfill.

What are Records and Tuples in Javascript?

Records and Tuples introduce a brand new primitive type to Javascript, but ultimately follow the same syntax as Objects and Arrays. When we want to define a new Record or Tuple, we use the syntax #{} or #[]. As such, we can define both as shown in the code below:

let myRecord = #{
    name: "New Record",
    tags: #['some', 'tags', 'go', 'here']
}

let myTuple = #['some', 'other', 'set', 'of', 'array', 'items'];
Enter fullscreen mode Exit fullscreen mode

As you can see, the syntax is identical to objects and arrays, with the exception of the hash (#) at the start. Note, that we can also put a Tuple in our Record, as shown in the first example.

Records and Tuples are immutable

Both Records and Tuples in Javascript are deeply immutable. All that means is that they can't be changed, at any level. If you try to change them, you'll get an error:

let myRecord = #{
    name: "New Record",
    tags: #['some', 'tags', 'go', 'here']
}

myRecord.name = 'Another Record'; // This will throw an error
Enter fullscreen mode Exit fullscreen mode

That also means you can't insert something new into them. In that way, they act as a source of truth - which brings us onto their main use case. Both Tuples and Records allow us to compare Objects and Arrays based on their value, rather than their identity.

Records and Tuples compare Values, not Identity

If you try to run the following code, you'll get false back:

console.log({ a: 1 } === { a: 1 }) // returns false
console.log([1, 2, 3] === [1, 2, 3]) // returns false
Enter fullscreen mode Exit fullscreen mode

That might be confusing, but it's because equality of Objects and Arrays work on the basis of identity. When we define a new Object or Array, it is given a unique identity. As such, when comparing the identity of two different Objects, the result is false.

Records and Tuples break that convention, and allow us to compare by value. Deep comparisons of objects has been something that's been quite tricky in Javascript for a long time, but with Tuples and Records we can finally do that. As such, the following code returns true:

console.log(#{ a: { b : 3 }} === #{ a: { b : 3 }}) // return true
console.log(#[1, 2, 3] === #[1, 2, 3]) // returns true
Enter fullscreen mode Exit fullscreen mode

This means we can finally (and easily) make comparisons of the value between different objects, where we expect a very specific return value.

Converting Arrays to Tuples and Objects to Records in Javascript

Since Records and Tuples are compared by value, you may want to convert regular Objects and Arrays into them, so that you can make that comparison. Fortunately, we can convert any Object or Array into a Record or Tuple using Record() and Tuple():

let newRecord = Record({ a: 1, b: 2 });
let newTuple = Tuple(...[1, 2, 3, 4, 5]);
let anotherTuple = Tuple.from([1, 2, 3, 4, 5]);
Enter fullscreen mode Exit fullscreen mode

Both the above lines will produce the Record and Tuple version of each. Future proposals also include a JSON.parseImmutable function, which will let us convert strings of Arrays or Objects straight into their Tuple or Record form. This is not currently implemented.

Adding to a Tuple or Record

I am aware that I have just said that you can't add to or change a Tuple/Record - but you can produce a new Tuple or Record based on an old one. This will be a totally different Tuple/Record - but it will use the data from the old and add something new. We can do that as shown below:

let myTuple = #[1, 2, 3, 4, 5];
let myRecord = #{ a: 1, b: 1 };

// Produce a new record using original myRecord:
let newRecord = #{ ...myRecord, c: 1 } // #{ a: 1, b: 1, c: 1}
// Produce a new tuple using myTuple:
let newTuple = #[ ...myTuple, 6, 7];
// Or you can use Tuple.with():
let anotherTuple = myTuple.with(6, 7);
Enter fullscreen mode Exit fullscreen mode

Interacting with Tuples and Records in Javascript

Tuples and Records work exactly the same as Objects and Arrays except they cannot be changed. As such, you can iterate through them, or interact with them using the same methods as are available on Objects and Arrays. For example, we could get all the keys of a particular Record:

let myRecord = #{ a: 1, b: 2, c: 2};
let recordKeys = Object.keys(myRecord); // Returns ['a', 'b', 'c'];
Enter fullscreen mode Exit fullscreen mode

Or you could iterate over an array using a for loop:

let myTuple = #[1, 2, 3, 4, 5];
for(const i of myTuple) {
    console.log(i);
}

// Console logs 1, 2, 3, 4, 5 on after each other
Enter fullscreen mode Exit fullscreen mode

Conclusion

As Tuples and Records aren't widely supported, you will need the Babel polyfill to use them. They allow us to compare data from Objects and Arrays in ways we couldn't before, making code much simpler where it once required complicated custom functions. To read the full proposal, click here.

Discussion (12)

Collapse
aandraz profile image
Andraž Kos
[1,2] === [1,2]
false

{1:2} === {1:2}
false

JSON.stringify([1,2]) === JSON.stringify([1,2])
true

JSON.stringify({1:2}) === JSON.stringify({1:2})
true
Enter fullscreen mode Exit fullscreen mode

[drops 🎤]

Collapse
smpnjn profile image
Johnny Simpson Author

You solved Javascript

Collapse
opensas profile image
opensas • Edited on

This is great news, but I wonder if there's a reason why we can't have something like this instead of the "#" thing

val myRecord = {
    name: "New Record",
    tags: ['some', 'tags', 'go', 'here']
}

val myTuple = ['some', 'other', 'set', 'of', 'array', 'items'];
Enter fullscreen mode Exit fullscreen mode
Collapse
smpnjn profile image
Johnny Simpson Author

You can use Record() and Tuple() afaik if you have that preference

Collapse
opensas profile image
opensas

Yes, I understood it, I just think it would be more elegant and clear.
Here's my message on the thread discussing this proposal:
twitter.com/opensas/status/1499729...

Collapse
petsel profile image
Peter Seliger • Edited on

The most obvious reason is to be able to pass records and tuples directly to functions as literals. Thus, in order to let the engine know how to handle such data, it needs its own syntax. Just another variable keyword doesn't cover this in future very common use case.

Collapse
mdmazzola profile image
Matt Mazzola • Edited on

Good post. Given the mutation operations on records and tuples are common way to simulate immutable behavior now hopefully the transition will be as simple adding # sign in certain cases.

It would be interesting to see the performance differences between the generation of new object using spread vs generating new record using spread and likewise with tuples.

Based on your first example of a record nested in record, it seems that there would be benefit to having a "deep record" symbol to implicitly make nested arrays or objects also become records or tuples.

Collapse
pez profile image
Peter Strömberg

The talk ”Solving Problems the Clojure Way”, by Rafal Dittwald is also relevant here since he is using JavaScript like if the objects and arrays were immutable in the example: youtube.com/watch?v=vK1DazRK_a0

Collapse
rajeshdixitsatyanarayan profile image
Rajesh Dixit

This looks great but I wonder the reasoning behind this. We can still achieve this using Object.freeze, so what is the benefit? Only thing I can see is compare by value but that does not seem that big of a reason to create new datatypes in a language. One thing I can think would help though is using Set/ Map with these. You can store complex record and to fetch it, all you need is set.get(#{...}).

Collapse
amymc profile image
amy mccarthy • Edited on

Object.freeze isn't deeply immutable. From the MDN docs:

"The result of calling Object.freeze(object) only applies to the immediate properties of object itself and will prevent future property addition, removal or value re-assignment operations only on object. If the value of those properties are objects themselves, those objects are not frozen and may be the target of property addition, removal or value re-assignment operations."

Collapse
pez profile image
Peter Strömberg

This is great news! Anyone who wants to understand just how great should watch Rich Hickey's talk ”The Value of Values”: youtube.com/watch?v=-I-VpPMzG7c

Collapse
leo profile image
Leonardo Galante

Great post @smpnjn can you do a spread of a normal object in a record? same for arrays?