TC39 proposal: Record and Tuples, the real immutable data structures in JavaScript.
One of the misleading use cases for beginners in JavaScript is to create constant arrays and objects. With the introductions of ES6, we got two new declarators: let
, for mutable variables, and const
, for constants. Many beginners believe that this will make their objects and array immutable, to discover later that they are not. The object or array itself is immutable, but not their content. So far, we have relied on the method Object.freeze
to deal with this use case.
// This is supposed to be immutable, isn't it?
const obj = { a: 1 }obj.a = 2
console.assert(obj.a === 1, 'wtf')
// Assertion failed: wtf
Introducing Record and Tuples.
Records are immutable arrays. Tuples are immutable objects. They are compatible with Object and Array methods. Essentially, you can drop a Tuple or a Record in any method that takes an object, or an array and it will behave as expected, unless this implies to modify the element. This applies to method of the standard library, iterators, etc.
// Record
const record = #{ x: 1, y: 2 }// Tuple
const tuple = #[1, 2, 3, 4]// We can use most of the methods that work with Arrays and Objects.
console.assert(tuple.includes(1) === true, 'OK, it will not print')// Although they will return tuples and records.
console.assert(Object.keys(record) === #['x', 'y'], 'OK, it will not print')// Iterators work too
for (const element of tuple) {
console.log(element)
}
// 1
// 2
// 3
// 4// And you can nest them!
const nested = #{
a: 1,
b: 2,
c: #[1, 2, 3]
}// Nope
tuple.map(x => doSomething(x));
// TypeError: Callback to Tuple.prototype.map may only return primitives, Records or Tuples
// This is ok
Array.from(tuple).map(x => doSomething(x))
However, with great power comes great responsibility.
The power
- Comparison by value: Like other simple primitive types, they are compared by value, not by identity. Objects and arrays are equal if they are the same entity. Tuples and records are equal if they contain the same elements.
const objA = {a: 1}
const objB = {a: 1}
const objC = objAconsole.assert(objA === objB, 'Same content, but different entities, false')
console.assert(objA === objC, 'They are the say, it will not print')const recordA = #{a: 1}
const recordB = #{a: 1}
const recordC = recordAconsole.assert(recordA === recordB, 'OK, will not print')
console.assert(recordA === recordC, 'OK, will not print')
- You can convert to objects and array and the other way around: Using the functions
Record()
andTuple.from()
.
const obj = { ...#{a: 1, b: 2}}
const record = Record({a:1, b:2})const arr = [ ...#[1, 2, 3]]
const tuple = Tuple.from([1, 2, 3])
- They are identified as distinct types: Using the operator
typeof
returns unique names for each of them.
console.assert(typeof #{a: 1} === 'record', 'this will not print')
console.assert(typeof #[1, 2] === 'tuple', 'this will not print')
The responsibility
- They can only contain primitive types: They can only contain String, Number, Boolean, Symbol, BigInt, undefined, null, Record and Tuple. This is, no functions, objects, arrays, classes, etc.
- You can use them in Maps and Sets, but not with WeakMaps and WeakSets: Quoting from the spec
It is possible to use a
Record
orTuple
as a key in aMap
, and as a value in aSet
. When using aRecord
orTuple
here, they are compared by value.It is not possible to use a
Record
orTuple
as a key in aWeakMap
or as a value in aWeakSet
, becauseRecords
andTuple
s are notObjects
, and their lifetime is not observable.
JSON.stringify
will work as expected, butJSON.parse
will still return objects and arrays: There is a proposal to addJSON.parseImmutablee
which will behave likeJSON.parse
but returning records and tuples instead of arrays and objects.
Conclusion
This addition is welcomed as it has been a struggle to define immutable values in JavaScript, and confusing for many beginners. Prior solutions implied using external libraries like Immutable.js ,workarounds in the standard library like Object.freeze
or conventions to achieve similar results.
The proposal is currently on stage 2, so it is subject to changes. However, it already looks solid and I personally hope it makes it through and becomes a standard.
References
- Proposal spec [github, web]
- Playground (beware, the current specification is not fully implemented)
- Tutorial
- Cookbook