Record
and Tuple
proposal: An overviewThe 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.
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.
Record
and Tuple
proposalThe 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.
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.
Let’s explore these new data types in greater detail below.
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.
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 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]
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 aWeakSet
. 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.
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.
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"
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']
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.
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/tuplesThe 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.
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.
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 see exactly what the user did that led to an error.
LogRocket records console logs, page load times, stack traces, slow network requests/responses with headers + bodies, browser metadata, and custom logs. Understanding the impact of your JavaScript code will never be easier!
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowBuild scalable admin dashboards with Filament and Laravel using Form Builder, Notifications, and Actions for clean, interactive panels.
Break down the parts of a URL and explore APIs for working with them in JavaScript, parsing them, building query strings, checking their validity, etc.
In this guide, explore lazy loading and error loading as two techniques for fetching data in React apps.
Deno is a popular JavaScript runtime, and it recently launched version 2.0 with several new features, bug fixes, and improvements […]
2 Replies to "The JavaScript <code>Record</code> and <code>Tuple</code> proposal: An overview"
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
Wouldn’t it be simpler to have ECMA allow casting any variable as constants instead of creating a new syntax, new types ?