Records and Tuples in JavaScript
Records and Tuples are a proposed feature for JavaScript that introduces deeply immutable data structures. They provide value-based equality checking rather than the reference-based equality of objects and arrays.
The Problem They Solve
JavaScript’s built-in objects and arrays have reference-based equality, which can lead to unexpected behavior:
// Reference-based equality with objects
const obj1 = { x: 1, y: 2 };
const obj2 = { x: 1, y: 2 };
console.log(obj1 === obj2); // false, despite having the same content
// Same issue with arrays
const arr1 = [1, 2, 3];
const arr2 = [1, 2, 3];
console.log(arr1 === arr2); // false, despite having the same content
Basic Concept
Records and Tuples introduce two new immutable data structures:
- Record: Immutable key-value pairs (similar to objects)
- Tuple: Immutable ordered lists (similar to arrays)
// Proposed syntax for Records and Tuples
const record = #{ x: 1, y: 2 }; // Record literal with # prefix
const tuple = #[1, 2, 3]; // Tuple literal with # prefix
// Value-based equality
const record2 = #{ x: 1, y: 2 };
console.log(record === record2); // true, because they have the same content
const tuple2 = #[1, 2, 3];
console.log(tuple === tuple2); // true, because they have the same content
Current Status
Records and Tuples are currently at Stage 2 in the TC39 proposal process. This means the feature is still being developed and is not yet part of the ECMAScript standard.
Key Characteristics
Immutability
Records and Tuples are deeply immutable:
const record = #{ x: 1, y: 2 };
// record.x = 3; // Error: Cannot assign to read-only property 'x'
const tuple = #[1, 2, 3];
// tuple[0] = 4; // Error: Cannot assign to read-only property '0'
// tuple.push(4); // Error: tuple.push is not a function
Value-Based Equality
Equality checks compare values, not references:
// Records with the same content are equal
console.log(#{ a: 1, b: 2 } === #{ a: 1, b: 2 }); // true
// Order of properties doesn't matter for Records
console.log(#{ a: 1, b: 2 } === #{ b: 2, a: 1 }); // true
// Tuples with the same content are equal
console.log(#[1, 2, 3] === #[1, 2, 3]); // true
// Order matters for Tuples
console.log(#[1, 2, 3] === #[3, 2, 1]); // false
Nested Records and Tuples
Records and Tuples can be nested, and equality checks are deep:
const nested1 = #{
point: #{ x: 1, y: 2 },
values: #[3, 4, 5]
};
const nested2 = #{
point: #{ x: 1, y: 2 },
values: #[3, 4, 5]
};
console.log(nested1 === nested2); // true, deep equality check
Valid Record Keys and Tuple Values
Records and Tuples can only contain primitive values or other Records and Tuples:
// Valid Record and Tuple values
const valid = #{
string: "text",
number: 42,
boolean: true,
null: null,
undefined: undefined,
symbol: Symbol("description"),
bigint: 42n,
record: #{ nested: true },
tuple: #[1, 2, 3]
};
// Invalid: Cannot contain objects or functions
// const invalid = #{ obj: {}, arr: [], func: () => {} }; // Error
Creating Records and Tuples
Literal Syntax
// Record literal
const point = #{ x: 1, y: 2 };
// Tuple literal
const coordinates = #[10, 20, 30];
Conversion from Objects and Arrays
// Convert from object to Record
const obj = { a: 1, b: 2 };
const record = Record(obj);
console.log(record); // #{a: 1, b: 2}
// Convert from array to Tuple
const arr = [1, 2, 3];
const tuple = Tuple(arr);
console.log(tuple); // #[1, 2, 3]
// Deep conversion
const complex = {
point: { x: 1, y: 2 },
values: [3, 4, 5]
};
const complexRecord = Record.fromEntries(
Object.entries(complex).map(([k, v]) => {
if (Array.isArray(v)) return [k, Tuple(v)];
if (typeof v === 'object' && v !== null) return [k, Record(v)];
return [k, v];
})
);
console.log(complexRecord); // #{point: #{x: 1, y: 2}, values: #[3, 4, 5]}
Working with Records and Tuples
Creating Modified Copies
Since Records and Tuples are immutable, you need to create new ones to make changes:
// Creating a new Record with modified values
const original = #{ x: 1, y: 2 };
const updated = #{...original, y: 3 }; // Spread syntax works
console.log(updated); // #{x: 1, y: 3}
// Creating a new Tuple with modified values
const numbers = #[1, 2, 3];
const moreNumbers = #[...numbers, 4, 5]; // Spread syntax works
console.log(moreNumbers); // #[1, 2, 3, 4, 5]
// Replacing a value in a Tuple
const replaceAt = (tuple, index, value) => {
return Tuple([...tuple.slice(0, index), value, ...tuple.slice(index + 1)]);
};
const modified = replaceAt(#[1, 2, 3], 1, 42);
console.log(modified); // #[1, 42, 3]
Accessing Properties and Elements
// Accessing Record properties
const user = #{ name: "Alice", age: 30 };
console.log(user.name); // "Alice"
console.log(user["age"]); // 30
// Destructuring works
const { name, age } = user;
console.log(name, age); // "Alice" 30
// Accessing Tuple elements
const point3D = #[10, 20, 30];
console.log(point3D[0]); // 10
console.log(point3D[1]); // 20
// Destructuring works
const [x, y, z] = point3D;
console.log(x, y, z); // 10 20 30
Iteration
Records and Tuples are iterable:
// Iterating over a Record
const record = #{ a: 1, b: 2, c: 3 };
for (const [key, value] of Object.entries(record)) {
console.log(key, value);
}
// "a" 1
// "b" 2
// "c" 3
// Iterating over a Tuple
const tuple = #[10, 20, 30];
for (const value of tuple) {
console.log(value);
}
// 10
// 20
// 30
// Using array methods via Array.from
const doubled = Array.from(tuple).map(x => x * 2);
console.log(doubled); // [20, 40, 60] (regular array)
Practical Applications
As Object Keys
Records and Tuples can be used as object keys, unlike objects and arrays:
// Using Records as Map keys
const pointMap = new Map();
pointMap.set(#{ x: 1, y: 2 }, "Point A");
pointMap.set(#{ x: 3, y: 4 }, "Point B");
// We can retrieve by value
console.log(pointMap.get(#{ x: 1, y: 2 })); // "Point A"
// Using Tuples as Map keys
const coordinateMap = new Map();
coordinateMap.set(#[0, 0], "Origin");
coordinateMap.set(#[1, 0], "East");
console.log(coordinateMap.get(#[0, 0])); // "Origin"
Immutable State Management
Records and Tuples are ideal for state management in applications:
// Application state as a Record
let state = #{
user: #{
id: 1,
name: "Alice",
preferences: #{
theme: "dark",
notifications: true
}
},
items: #[
#{ id: 1, name: "Item 1", completed: false },
#{ id: 2, name: "Item 2", completed: true }
]
};
// Update state immutably
function updateTheme(currentState, newTheme) {
return #{
...currentState,
user: #{
...currentState.user,
preferences: #{
...currentState.user.preferences,
theme: newTheme
}
}
};
}
// Apply update
state = updateTheme(state, "light");
console.log(state.user.preferences.theme); // "light"
Configuration Objects
Records are perfect for configuration that shouldn’t change:
// Application configuration as a Record
const config = #{
api: #{
baseUrl: "https://api.example.com",
timeout: 5000,
retries: 3
},
features: #{
darkMode: true,
analytics: true,
experimental: #[
"feature1",
"feature2"
]
}
};
// Safe to pass around without worry of mutation
function initializeApp(appConfig) {
// appConfig is guaranteed to be immutable
console.log(`Connecting to ${appConfig.api.baseUrl}`);
// ...
}
initializeApp(config);
Caching and Memoization
Value-based equality makes Records and Tuples ideal for caching:
// Memoization with Records as cache keys
const memoize = (fn) => {
const cache = new Map();
return (...args) => {
const key = Tuple(args);
if (cache.has(key)) {
return cache.get(key);
}
const result = fn(...args);
cache.set(key, result);
return result;
};
};
// Usage
const expensiveCalculation = memoize((a, b, c) => {
console.log("Calculating...");
return a * b * c;
});
console.log(expensiveCalculation(2, 3, 4)); // Logs "Calculating..." then 24
console.log(expensiveCalculation(2, 3, 4)); // Just returns 24 (from cache)
Comparison with Existing Solutions
Records vs. Objects
// Regular object
const obj = { x: 1, y: 2 };
obj.z = 3; // Mutable
const obj2 = { x: 1, y: 2 };
console.log(obj === obj2); // false (reference equality)
// Record
const rec = #{ x: 1, y: 2 };
// rec.z = 3; // Error: Cannot add property z, object is not extensible
const rec2 = #{ x: 1, y: 2 };
console.log(rec === rec2); // true (value equality)
Tuples vs. Arrays
// Regular array
const arr = [1, 2, 3];
arr.push(4); // Mutable
const arr2 = [1, 2, 3];
console.log(arr === arr2); // false (reference equality)
// Tuple
const tup = #[1, 2, 3];
// tup.push(4); // Error: tup.push is not a function
const tup2 = #[1, 2, 3];
console.log(tup === tup2); // true (value equality)
Comparison with Object.freeze()
// Frozen object
const frozen = Object.freeze({ x: 1, y: { z: 2 } });
// frozen.x = 3; // Error in strict mode
frozen.y.z = 3; // Works! Only shallow immutability
// Record (deeply immutable)
const record = #{ x: 1, y: #{ z: 2 } };
// record.x = 3; // Error
// record.y.z = 3; // Error
Comparison with Immutable.js
// Immutable.js
import { Map, List } from 'immutable';
const map = Map({ x: 1, y: 2 });
const newMap = map.set('z', 3); // Returns new immutable map
console.log(map.equals(Map({ x: 1, y: 2 }))); // true (value equality)
// Records and Tuples
const rec = #{ x: 1, y: 2 };
const newRec = #{...rec, z: 3}; // Creates new record
console.log(rec === #{ x: 1, y: 2 }); // true (value equality)
Current Workarounds
Until Records and Tuples are widely supported, there are several alternatives:
Deep Freezing Objects
// Deep freeze function
function deepFreeze(obj) {
if (obj === null || typeof obj !== 'object') return obj;
Object.keys(obj).forEach(key => {
const value = obj[key];
if (typeof value === 'object' && value !== null) {
deepFreeze(value);
}
});
return Object.freeze(obj);
}
// Usage
const frozenObj = deepFreeze({
point: { x: 1, y: 2 },
values: [3, 4, 5]
});
// frozenObj.point.x = 10; // Error in strict mode
Immutable Libraries
// Using Immutable.js
import { Map, List } from 'immutable';
const state = Map({
user: Map({
name: 'Alice',
preferences: Map({
theme: 'dark'
})
}),
items: List([
Map({ id: 1, text: 'Item 1' }),
Map({ id: 2, text: 'Item 2' })
])
});
// Update immutably
const newState = state.setIn(['user', 'preferences', 'theme'], 'light');
Simulating Value Equality
// Deep equality function
function deepEqual(a, b) {
if (a === b) return true;
if (a === null || b === null ||
typeof a !== 'object' || typeof b !== 'object') {
return false;
}
const keysA = Object.keys(a);
const keysB = Object.keys(b);
if (keysA.length !== keysB.length) return false;
return keysA.every(key => {
if (!keysB.includes(key)) return false;
return deepEqual(a[key], b[key]);
});
}
// Usage
const obj1 = { x: 1, y: { z: 2 } };
const obj2 = { x: 1, y: { z: 2 } };
console.log(deepEqual(obj1, obj2)); // true
Best Practices
- Use for Immutable Data: Records and Tuples are ideal for data that should never change
- Consider Performance: Value equality checks are more expensive than reference checks
- Plan for Transition: Design code that can work with both regular objects/arrays and Records/Tuples
- Use Typed Arrays for Binary Data: Records and Tuples are not suitable for large binary data
- Combine with Existing Patterns: Records and Tuples complement patterns like Redux and immutable state management
// Transition strategy
function safelyUseRecordOrObject(data) {
// Check if Records are supported
const supportsRecords = typeof Record === 'function';
if (supportsRecords) {
// Use Record if available
return Record(data);
} else {
// Fall back to frozen object
return Object.freeze({...data});
}
}
Browser and Environment Support
Records and Tuples are still in the proposal stage (Stage 2 in the TC39 process as of the latest update). Support varies across environments:
- Browsers: Not yet natively supported in major browsers
- Node.js: Not yet natively supported
- Babel: Support via the
@babel/plugin-proposal-record-and-tuple
plugin - TypeScript: Not yet supported natively
// Using Babel plugin
// babel.config.js
module.exports = {
plugins: [
['@babel/plugin-proposal-record-and-tuple', {
importPolyfill: true,
syntaxType: 'hash'
}]
]
};
// TypeScript workaround with utility types
type Record<T> = Readonly<T> & { readonly __record__: unique symbol };
type Tuple<T> = ReadonlyArray<T> & { readonly __tuple__: unique symbol };
// Usage
function createRecord<T>(obj: T): Record<T> {
return Object.freeze({...obj}) as Record<T>;
}
function createTuple<T>(arr: T[]): Tuple<T> {
return Object.freeze([...arr]) as Tuple<T>;
}
Interview Tips
- Explain the key differences between Records/Tuples and Objects/Arrays
- Describe the benefits of value-based equality for data structures
- Discuss use cases where Records and Tuples would be particularly beneficial
- Explain how Records and Tuples relate to functional programming concepts
- Discuss current workarounds and alternatives until Records and Tuples are standardized
- Demonstrate knowledge of the TC39 proposal process and the current status of the proposal
Test Your Knowledge
Take a quick quiz to test your understanding of this topic.