Alexander Nnakwue Software engineer. React, Node.js, Python, and other developer tools and libraries.

The JavaScript Record and Tuple proposal: An overview

7 min read 2006

JavaScript Logo

Introduction

The ECMAScript Record and Tuple proposal introduces two new data structures to JavaScript: records and tuples. These data structures would allow us to create the first two compound primitive values in JavaScript.

Compound primitives are composed of other constituent values, which means they can contain simple primitives like strings, numbers, and Booleans, as well as compound primitives themselves — i.e., records and tuples.

Primitives, including compound primitives types, share a couple of distinct features. Firstly, they are deeply immutable, which means we can’t alter them the way we can alter non-primitives (e.g., objects) since they return new values altogether and not copies of their original values.

Also, due to their deeply immutable nature, we can compare them using the strict equality operator (===) with even more guarantees. This means these data types can be compared strictly by their contents, and we can be certain they are equal if they contain the same constituent elements.

Previous advances on immutability in JavaScript

An ECMAScript proposal on immutable data structures was previously considered, but it was ultimately abandoned due to some associated complexities and a lack of sufficient use cases.

These days, users rely on libraries like Immutable.js and Immer to handle deep immutability for objects and arrays in the language. Immer’s approach, for example, relies on generating frozen objects. Using these libraries can lead to some potential issues, however.

Firstly, there are different ways of doing the same thing that do not interoperate well. In addition, the syntax used in these libraries is not as ergonomic as it could be if it was integrated natively in JS. Finally, it can be difficult to get these libraries to work well with external type systems.

With this new Record and Tuple proposal, there is a sure and standard way of handling things since it is based only on primitives. By designing records and tuples to be based on primitives alone, the proposal defines a clear-cut way for comparison while removing the complexities introduced by these libraries.

A closer look at the Record and Tuple proposal

The proposal is currently in stage 2 of the TC39 process, which means it is still a work in progress and is likely to change based on community feedback. It has been spearheaded by TC39 members Robin Ricard and Rick Button of Bloomberg.

Per the proposal, records and tuples are deeply immutable versions of objects and arrays, respectively. In essence, records were designed to have an object-like structure, while tuples are array-like in structure. And as we mentioned earlier, records and tuples can only contain primitives, including other records and tuples.

Syntax

Records and tuples are currently declared using a preceding # modifier. This is what differentiates them from object and array declarations in the language. Let’s see some examples of the currently proposed syntax for defining these new data types.

Record declarations:

const rec1 = #{} // an empty record
const rec2 = #{ a: 1, b: 2 } // a record containing two KV pairs
const rec3 = #{ a: 1, b: #[2, 3] } // a record with two elements including a tuple containing 2 elements

Tuple declarations:

const tup1 = #[]  // an empty tuple
const tup2 = #[1, 2]  // a tuple containing two elements
const tup3 =#[1, 2, #{ a: 3 }] // a tuple with three elements including a record containing 1 element

Although the proposed syntax is already used elsewhere in the language (e.g., private class fields), it is similar to the syntax for both object and array literals, so it wouldn’t be too difficult for users to understand. With that said, there are discussions around using a new keyword entirely, or using a totally different syntax with {| |} and [||].

Note: For details on possible syntax errors, check this section of the proposal document.

Exploring records and tuples

Let’s explore these new data types in greater detail below.

Records

As we’ve mentioned, records are similar to objects, but they are deeply immutable. The syntax for records is similar to the way we’d define objects, with a preceding # symbol. Let’s declare a sample record below:

const rec1 = #{ a: 10, b: 20, c: 30 }

Again, note that only primitives types are acceptable as properties in records. Therefore, we can have tuples and other records inside a record since they are all primitives. For example:

const rec2 = #{x: #[1,2,3], y: #{ a: 10, b: 20, c: 30 }}

Note: Attempting to create a record or tuple that contains any type except primitive data types results in a typeError. More details about the design decisions for deep immutability can be found here.

Object methods with records

We can also make use of object methods with records. For example, let’s use the object spread syntax on the above example:

const rec3 = #{x: #[1,2,3], ...rec2}

console.log(rec3) 
// rec3 return value
#{x: Tuple, y: Record}
        1. ▶x: Tuple
            1.  0: 1
            2.  1: 2
            3.  2: 3
        2. ▶y: Record
            1.  a: 10
            2.  b: 20
            3.  c: 30

As another example, let’s extract the keys of the above rec3 record above by logging it to the console on the playground.

console.log(Object.keys(rec3))
// ["x", "y"]
   1.  0: "x"
   2.  1: "y"

We can also apply destructuring on records using the standard method, as shown below:

const {name, ...rest} = #{ name: "Alex", occupation: "Farmer", age: 98 };

console.log(name); // Alex
console.log(rest); // Object {age: 98, occupation: "Farmer"}
console.log(#{...rest}); // Record #{age: 98, occupation: "Farmer"}

And just as we can access properties in regular objects, we can do the same with records:

console.log(rec3.x); // #[1, 2, 3]

Tuples

Tuples are similar to arrays in JavaScript, but, again, they are deeply immutable. Let’s take another look at their syntax:

const tup1 = #[1, 2, 3, 4]

Array methods with tuples

In the same way records support object methods, tuples support array methods. For example, we can access the position or indices of elements just as we would with arrays:

console.log(tup1[1]) // 2

We can make use of the spread operator to combine two tuples as well:

const tup2 = #[5,6,7,8,9]

const tup3 = #[...tup1, ...tup2];

console.log(tup3) // #[1, 2, 3, 4, 5, 6, 7, 8, 9]

Tuples also support standard array methods like map:

const tup = #[1, 2, 3]
console.log(tup.map(x => x * 2)); 
// #[1, 2, 3] 

Note: The callback to Tuple.prototype.map may only return primitives.

Likewise, we can apply destructuring on tuples using the standard method below:

const [head, ...rest] = #[1, 2, 3];

console.log(head); // 1
console.log(rest); // Array [2, 3]
console.log(#[...rest]); // Tuple #[2, 3]

In general, objects and arrays support the same methods for working effectively with records and tuples in JavaScript, though there are subtle differences in some cases, which we will explore later.

Note: Records and tuples are equally important as keys of maps and as elements of sets. According to the proposal, maps and sets become more powerful when used together with records and tuples due to the nature of primitive types in the language.

Alternatively, records and tuples cannot be used as keys in a WeakMap or as values in a WeakSet. This is because it rarely makes sense to use them specifically as keys in (non-weak) maps, as primitives are not allowed.

We should also note that only object types should be used in these cases because they are non-primitives. More details on this topic can be found here in the proposal document.

Equality of record/tuple data types

With this new proposal, we can easily compare compound primitives by value, unlike objects or arrays, which can only be compared by reference or identity. Let’s see some examples using records and tuples below.



Comparing tuples and records:

console.log(#{x: 1, y: 4} === #{y: 4, x: 1})
//true

console.log(#['a', 'b'] === #['a', 'b'])
//true

Comparing objects and arrays:

console.log({x: 1, y: 4} === {x: 1, y: 4})
//false 

console.log(["a", "b"] === ["a", "b"])
//false

As we can see, records and tuples are always equal to each other when compared. Objects and arrays, on the other hand, are not equal because they are non-primitives, as previously discussed.

In essence, if the structure and contents of records and tuples are identical, the proposal states, then their values are considered equal according to the strict equality (===) operations. Also, as we can see from the example above, the insertion order of record keys does not affect equality of records, unlike with objects.

Note: Strict equality is important for these data types so that users don’t need to worry about which record/tuple is being manipulated or where it was created; in other words, it ensures predictable behavior.

Iterating through tuples and records

Just like arrays, tuples are iterable:

const tup = #[1,2,3]

for (const o of tup) { 
console.log(o);  
}

// 1,2,3

Alternatively, similar to objects, records are only iterable in conjunction with APIs like Object.entries:

const rec = #{z: 1, a: 2 }

// Object.entries can be used to iterate over Records, just like with Objects
for (const [key, value] of Object.entries(rec)) { 
console.log(key) 
}

//  1. "a" 2. "z"

Converting records and tuples to plain objects/arrays

In order to convert a record back to an object in JS, all we need to do is wrap it around an Object constructor:

const rec = #{x: 1, y: 4})
console.log(Object(rec)
// returns an Object {x: 1, y: 4} 

Likewise, to convert a tuple to an array, all we need to do is use the Array.from method:

const tup = #['a', 'b']
console.log(Array.from(tup))
// returns an array  ['a', 'b']

Converting from objects and arrays

We can convert objects and arrays to records and tuples using the Record() and Tuple.from() methods, respectively. Note that Record() and Tuple.from() would only work with records, tuples, or other primitives. Let’s see some examples.

For records:

const obj = { a: 1, b: 2, c: 3 }
const rec1 = Record(obj);
console.log(rec1)
//#{ a: 1, b: 2, c: 3 }

For tuples:

const arr = [1, 2, 3]
const tup = Tuple.from(arr); 
console.log(tup)
//#[1, 2, 3]

Note: Nested object references would cause a TypeError as the current draft proposal does not contain recursive conversion routines.

Array-like manipulations with Tuple

In this case, Tuple.prototype.pushed is similar to using Array.prototype.push. However, when it comes to these operations on tuples, they are immutable since they always return new modified versions:

const tup1 = #[1, 2];
console.log(tup1.pushed(3)) // #[1, 2, 3]

Similarly, the Tuple.prototype.sorted method is analogous to using the Array.prototype.sort method in the language:

const tup2 = #[3, 2, 1]
console.log(tup2.sorted) // #[1, 2, 3]

JSON.parseImmutable and JSON.stringify on records/tuples

The proposal adds JSON.parseImmutable, which would allow us to extract a record or a tuple out of a JSON string. It is analogous to how JSON.parse works on objects and arrays.

Note: At the time of writing, the playground does not support JSON.parseImmutable.

Also, the behavior of JSON.stringify on records and tuples is equivalent to how JSON.stringify acts on objects or arrays, respectively.

JSON.stringify on records:

const rec = #{ a: #[1, 2, 3] }

console.log(JSON.stringify(rec));

//"{"a":[1,2,3]}"

JSON.stringify on objects:

const obj = { a: [1, 2, 3] }

console.log(JSON.stringify(obj));

//"{"a":[1,2,3]}"

More details can be found in the proposal document. Also, all the examples for this article can be found here in the playground.

Conclusion

The Record and Tuple proposal is still a work in progress. At the moment, they are both experimental features intended to solve deep immutability natively. Nowadays, users rely on libraries like Immutable.js and Immer to handle cases of deep immutability. But, as we discussed previously, this can pose problems down the line.

This proposal design offers a guarantee against common programming mistakes, as everything in records and tuples are not like objects or arrays. The design ensures that records and tuples remain immutable.

In essence, the structures of records and tuples remain guaranteed as opposed to using Object.freeze(). As we know, Object.freeze only performs a shallow operation, and it also does not guarantee strict equality with objects or arrays. Therefore, with native deep immutability in the language, we don’t have to rely on libraries, which offer shallow cloning on objects or arrays.

In this introduction to the proposal, we have been able to cover the basic use cases for these data types and some examples of how we’d use them. For more info, you can find links to the proposal, including the spec, cookbook, and official tutorial, on GitHub.

You can also check out a follow-up proposal that would add deep path properties for records. Finally, to practice the examples we have covered in this tutorial, check out the playground.

: Debug JavaScript errors more easily by understanding the context

Debugging code is always a tedious task. But the more you understand your errors the easier it is to fix them.

LogRocket allows you to understand these errors in new and unique ways. Our frontend monitoring solution tracks user engagement with your JavaScript frontends to give you the ability to find out exactly what the user did that led to an error.

LogRocket records console logs, page load times, stacktraces, slow network requests/responses with headers + bodies, browser metadata, and custom logs. Understanding the impact of your JavaScript code will never be easier!

.
Alexander Nnakwue Software engineer. React, Node.js, Python, and other developer tools and libraries.

2 Replies to “The JavaScript Record and Tuple proposal: An overview”

  1. avoiding from python complexity of arrays, chasing me. I just imagine myself trying to dig how to convert immutable output of an library to mutate

Leave a Reply