Records and Tuples: JavaScript’s New Immutable Data Types

    Craig Buckler
    Share

    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:

    1. They must not have holes with unset values. For example, #[1,,,4] is invalid.
    2. 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:

    1. They must use string property names. For example, #{ Symbol(): 1 } is invalid.
    2. 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.