Records and tuples are new JavaScript immutable data types currently at stage 2 in the TC39 standards approval process. They are subject to change and not currently available in any browser or runtime, but working implementations should arrive within the next year. They help solve a couple of confusing conundrums faced by coders …
Constant Changes
Professional JavaScripters will tell you that assigning variables with const
is best practice where possible. It makes variables immutable. Values can’t be changed, so you have fewer issues to deal with.
Unfortunately, const
only makes primitive values immutable (String, Number, BigInt, Boolean, Symbol, and undefined
). You can’t reassign an array or an object, but the values and properties they contain can be modified. For example:
// array constant
const myArray = [1, 2, 3];
// change array values
myArray[0] = 99;
myArray.push(42);
console.log(myArray); // [ 99, 2, 3, 42 ]
myArray = 'change'; // ERROR!
Similarly for objects:
// object constant
const myObj = { a: 1, b: 2, c: 3 }
// change object properties
myObj.a = 99;
myObj.d = 42;
console.log(myObj); // { a:99 ,b:2, ,c:3, ,d:42 }
myObj = 'change'; // ERROR!
The Object.freeze()
method can help, but only shallow freezing is applied to the immediate child properties of an object:
const myObj = { a: 1, b: 2, c: { v: 3 } }
Object.freeze(myObj);
myObj.a = 99; // silently ignored
myObj.c.v = 99; // works fine
console.log(myObj); // { a: 1, b: 2, c: { v: 99 } }
It’s therefore difficult to guarantee a function won’t intentionally or accidentally change the values held in an array or object. Developers must either hope for the best or pass a cloned version of a variable — (which has its own challenges).
Equivalent Inequality
Further chaos can ensue when developers attempt seemingly reasonable object or array comparisons:
const str = 'my string';
console.log( str === 'mystring' ); // true
const num = 123;
console.log( num === 123 ); // true
const arr = [1, 2, 3];
console.log( arr === [1, 2, 3] ); // false
const obj = { a: 1 };
console.log( obj === { a: 1 } ); // false
Only primitive types can be compared by value. Objects and arrays are passed and compared by reference. Two variables will only be equivalent when they point to the same item in memory:
const a = [1, 2];
const b = a;
b.push(3);
console.log( a === b ); // true
// original array has changed
console.log( a ); // [1, 2, 3]
Deeply comparing two objects or arrays requires a recursive comparison function to assess each value in turn. Even then, you may encounter issues with types such as dates or functions which could be stored in different ways.
Tuples: Immutable Array-like Data Structures
Tuples are deeply immutable array-like data structures. They are effectively compound primitive types identified with a #
modifier in front of normal array syntax:
// new tuples
const t1 = #[1, 2, 3];
const t2 = #[1, 2, #[3, 4]];
Alternatively, a new Tuple.from()
method can create a tuple from an array:
// new tuple from an array
const t3 = Tuple.from( [1, 2, 3] );
Unlike standard arrays, tuples have to satisfy these requirements:
- They must not have holes with unset values. For example,
#[1,,,4]
is invalid. - They must only set primitives, other tuples, or records. Types such as arrays, objects, or functions are not permitted:
const t4 = #[ new Date() ]; // ERROR (sets an object)
const t5 = #[1, 2, [3, 4]]; // ERROR (sets an array)
Since tuples are primitives, they can be deeply compared by value with other tuples:
const t6 = #[1, 2];
console.log( t6 === #[1, 2] ); // true
Note that comparisons using the less strict ==
operator are possible if the tuple holds a single value. For example:
const t7 = #[99];
console.log( t7 == #[99] ); // true
console.log( t7 == 99 ); // true
console.log( t7 == '99' ); // true
// tuple cannot be compared to an array
console.log( t7 == [99] ); // false
Records: Immutable Object-like Data Structures
Records are deeply immutable object-like data structures. Again, they are compound primitive types identified with a #
modifier in front of normal object syntax:
// new records
const r1 = #{ a: 1, b: 2 };
const r2 = #{
a: 1,
b: #{ c: 2 }, // child record
d: #[ 3, 4 ] // child tuple
};
Alternatively, the new Record()
constructor can create a record from an object:
// new record from an object
// #{ a: 1, b: 2 }
const r3 = Record({ a: 1, b: 2 });
Or the Record.fromEntries()
method can create a record from a series of array or tuple value-pairs:
// new record from array of name-values
// #{ a: 1, b: 2 }
const r4 = Record.fromEntries([
['a', 1],
['b', 2]
]);
Unlike standard objects, records must fulfill the following requirements:
- They must use string property names. For example,
#{ Symbol(): 1 }
is invalid. - They must only set values using primitives, other tuples, or records. Types such as arrays, objects, or functions are not permitted:
const r5 = #{ 'd': new Date() }; // ERROR (sets an object)
const r6 = #{ a: 1, b: { c: 2 } }; // ERROR (sets an object)
Records can be deeply compared with other records and the property order doesn’t matter:
const r7 = #{ a: 1, b: 2 };
console.log( r7 === #{ b: 2, a: 1 } ); // true
Records can only be compared to other records, so using a ==
or ===
operator makes no difference. However, it is possible to extract Object keys()
and values()
for specific comparisons. For example:
const r8 = #{ a: 99 };
console.log( Object.values(r8) == 99 ); // true
Immutable Updates
Tuples and records may sound like complex computer science terms, but they’ll finally permit robust immutable data storage and comparisons in JavaScript. You can try them out today in this playground, or with this polyfill, but please be aware that the proposed implementation could change in the coming months.
Records and Tuples in JavaScript FAQ
A Record in JavaScript is an object-like structure introduced with ECMAScript 2022 (ES12). It is designed for representing data with named properties and is immutable by default, making it suitable for use as a data structure where values do not change after creation.
Unlike regular objects, Records in JavaScript are immutable by default, and their properties cannot be modified once set. Records are also designed to be more predictable and have a more rigid structure, making them suitable for use as data containers.
A Tuple in JavaScript is an ordered collection of elements, where each element can be of a different type. Tuples are immutable and have a fixed length, providing a way to represent and work with a fixed number of values in a specific order.
Records and Tuples can enhance code readability by providing a more declarative and structured way to represent data. The immutability aspect also helps prevent unintended modifications, contributing to better code maintainability.
Records and Tuples, being immutable, can have performance benefits in certain scenarios. However, it’s crucial to consider browser support and the specific use case, as the performance impact may vary depending on the implementation details of the JavaScript runtime.
As of my last update, polyfills or transpilers may be used to emulate Records and Tuples in environments lacking native support. However, it’s recommended to stay informed about updates to JavaScript standards and tooling, as the ecosystem evolves.
Craig is a freelance UK web consultant who built his first page for IE2.0 in 1995. Since that time he's been advocating standards, accessibility, and best-practice HTML5 techniques. He's created enterprise specifications, websites and online applications for companies and organisations including the UK Parliament, the European Parliament, the Department of Energy & Climate Change, Microsoft, and more. He's written more than 1,000 articles for SitePoint and you can find him @craigbuckler.