Why was Records & Tuples proposal withdrawn in JavaScript?
Published on

As many people in JavaScript community know, recently the Records & Tuples proposal was withdrawn. Many people got really upset, some even to the point of blaming the EcmaScript standard committee. Here I want to discuss the possible reasons why this feature was discounted.
What was Records & Tuples proposal about?
Let's first understand what are Records & Tuples before going to deeper technical details.
Records & Tuples are 2 new object-like primitive types. Their 2 main features are:
- Immutability. They're read only after creation, any consequent modification is impossible.
- Are deeply compared when using operator
===
, unlike objects which are only compared by their references and can be still unequal even if their structures match.
This is how records and tuples are declared and basically work:
// Tuples are declared similarly to regular arrays, they just have # at the beginning
const tuple1 = #[1, 2, 4];
const tuple2 = #[1, 2, 3];
const tuple3 = #[1, 2, 3];
// Records are declared similarly to regular objects, they just have # at the beginning
const record1 = #{
a: 1,
b: tuple2,
};
const record2 = #{
a: 1,
b: tuple3,
};
const record3 = #{
a: 1,
b: tuple2,
c: #{
a: tuple2
}
};
console.log(typeof tuple1); // "tuple"
console.log(typeof record1); // "record"
console.log(tuple1 === tuple2); // false, their structures don't match
console.log(tuple2 === tuple3); // true, their structures match
console.log(record1 === record3); // false, their structures don't match
console.log(record1 === record2); // true, their structures match
const map = new Map();
map.set(record1, "record1"); // records and tuples can be even used for map keys
console.log(map.get(record2)); // "record1", since record1 equals record2
console.log(map.get(record3)); // undefined, since no key with an identical structure was found in the map
console.log(tuple1.length); // 3, will work like normal array
// Will iterate like on normal array, will print 1, 2, 4
for (const x of tuple1) {
console.log(x);
}
try {
// will cause an error on any modification attempt
tuple1[1] = 5;
} catch (e) {
console.error(e);
}
try {
// will cause an error on any modification attempt
record2['k'] = 5;
} catch (e) {
console.error(e);
}
try {
const record4 = #{
a: 1,
b: tuple2,
// will cause an error since the nested structures must also be immutable primitive values
c: {
a: tuple2
}
};
} catch (e) {
console.error(e);
}
You can experiment in this
playground. Unfortunately the typeof
keyword doesn't work correctly, because it's just a
polyfill and JavaScript itself doesn't support records and tuples.
Records and tuples can also be made from regular objects:
const record = Record({
a: 4
});
const tuple = Tuple.from([1, 2, 3]);
console.log(record, tuple); // #{ a: 4 }, #[1, 2, 3]
There are tons of other features, for more details check the proposal page.
The reasons for the withdrawal
Judging from these examples alone, records & tuples already seem to be an awesome feature, especially for state management. So why was this feature withdrawn?
Adding new fundamental types
The proposal adds 2 fundamental primitive types: record
and tuple
. Adding new
fundamental types is not the same thing as adding a new class or API. Fundamental types affect how the heart of
the JavaScript engines works, because JavaScript is a dynamic language, it has to constantly check and handle
different operations and coercions with and between different types. New fundamental types will add even more
complexity
and, possibly, performance overheads (even for code not
using records and tuples) to already complicated
JavaScript engines.
Deep comparison
Deep comparison also adds complexity in order to work efficiently. One possible optimization is to use structural sharing for the new objects as much as possible. This will attempt to use existing records / tuples as much as possible, which will limit the memory usage and potentially speed up the creation / comparison. Another possible optimization is to use hash values attached to the tuples / records, so this will help to quickly determine if 2 tuples/records are not equal in many cases. However, the consensus is that deep comparison is not guaranteed to work faster than in linear time. Optimizing it without interning looked hard, so implementers were unwilling to commit.
Consistency of equality semantics
Without value-based ===
, records and tuples break the
long-standing rule that ===
,
SameValue
(same as ===
except NaN === NaN
and 0 !== -0
), and
SameValueZero
(same as ===
except NaN === NaN
) coincide for
most types. Keeping that rule meant paying the complexity cost. Breaking it meant teaching the fifth equality
relation in JS. Neither option got traction. Also removing value based equality makes records and tuples almost
pointless 😒.
Composites might be a better alternative?
There were too many problems and uncertainties with records and tuples proposal in order to progress, thus the
feature was
eventually withdrawn. There is a new alternative proposal for Composites. Personally, I'm not very impressed with
the current
implementation yet. It still introduces some inconsistencies such as being compared structurally in
Map
/ Set
, but being compared by reference in WeakMap
/
WeakSet
:
const pos1 = Composite({ x: 1, y: 4 });
const pos2 = Composite({ x: 1, y: 4 });
Composite.equal(pos1, pos2); // true
const positions = new Set(); // the standard ES Set
const weakPositions = new WeakSet(); // the standard ES WeakSet
positions.add(pos1);
weakPositions.add(pos2);
positions.has(pos2); // true, OK, makes sense (at least we wanted this),
// but it's handled in a special way instead of the normal reference identity despite it's an object
weakPositions.has(pos2); // false, old behavior but now inconsistent with Set
Honestly, I don't know, maybe this is a necessary evil to have structural keys. Anyways, let's see where does this go.