wandres.dev
INMUTABILIDAD · structural sharing

El futuro: Records, Tuples y estructuras persistentes

Inmutabilidad a nivel de lenguaje: la propuesta Records and Tuples y su sucesora Composites, la diferencia entre inmutabilidad de compilación, de ejecución y semántica de valor, y lo que Clojure, Swift y Rust ya llevan años cosechando.

⏱ 16 min

Hoy la inmutabilidad en JavaScript es una disciplina que impones tú y sostienen las librerías. El horizonte es que sea una garantía del lenguaje: valores profundamente inmutables comparados por su contenido y no por su identidad. Ese es el objetivo de la propuesta Records and Tuples y de su sucesora, Composites. Entender hacia dónde va —y por qué el camino ha sido tan difícil— cierra el track de inmutabilidad situando lo que haces hoy dentro de una trayectoria que otros lenguajes ya recorrieron.

🎯 Al terminar esta lección sabrás
  • Comprender qué proponen los Records and Tuples y su igualdad por valor.
  • Conocer el estado real de la propuesta en 2026 y su sucesora Composites.
  • Separar inmutabilidad de compilación, de ejecución y semántica de valor.
  • Situar JavaScript frente a Clojure, Swift, Rust y Elixir.

Records y Tuples: inmutabilidad como primitivo

La propuesta Records and Tuples de TC39 añadía dos tipos primitivos profundamente inmutables: los records, objetos escritos con #{ }, y las tuples, arrays escritos con #[ ]. Su rasgo revolucionario no era solo la inmutabilidad, sino la igualdad por valor: dos records con el mismo contenido serían iguales con ===, como lo son hoy dos números iguales.

// Sintaxis PROPUESTA de Records and Tuples (no es JavaScript estandar aun)
const punto = #{ x: 1, y: 2 };        // record: objeto profundamente inmutable
const serie = #[1, 2, 3];             // tuple: array profundamente inmutable

#{ x: 1, y: 2 } === #{ x: 1, y: 2 };  // true  -> igualdad POR VALOR
punto.x = 9;                          // TypeError: es inmutable
const movido = #{ ...punto, x: 9 };   // version nueva, tambien inmutable

Piensa en lo que eso resuelve. Todo este track ha girado en torno a un truco: usar la identidad referencial como proxy del contenido. Con igualdad por valor, el truco se vuelve innecesario: la memoización “simplemente funciona” porque dos cálculos que producen el mismo contenido producen valores iguales, y una colección puede indexar por contenido en lugar de por identidad.

// Con igualdad por valor, un Map indexa por CONTENIDO, no por identidad.
const cache = new Map();
cache.set(#{ x: 1, y: 2 }, "resultado");
cache.get(#{ x: 1, y: 2 });   // "resultado" -> misma clave por valor
// Hoy, con objetos normales, esto devuelve undefined: distinta identidad.

La inmutabilidad dejaría de ser una convención vigilada para ser una propiedad del tipo, y con ella desaparecería una familia entera de bugs sutiles de memoización.

El estado real en 2026: de Records a Composites

Conviene ser honesto sobre el presente. La propuesta Records and Tuples, pese a su elegancia, se estancó: sobrecargar === para comparar por valor y hacer que el recolector de basura y los motores gestionaran valores compuestos inmutables con interning resultó enormemente complejo de implementar sin penalizar todo el lenguaje. El corazón del problema es ese interning: para que === compare records por valor en tiempo constante, el motor tendría que garantizar que dos records de igual contenido son el mismo objeto en memoria, lo que obliga a mantener una tabla global de valores y coordinarla con el recolector.

Ante ese muro, los promotores giraron hacia una propuesta más modesta y realista: Composites. No introduce sintaxis nueva ni toca ===. Ofrece una función Composite que crea un valor compuesto inmutable, y una comparación explícita Composite.equal para la igualdad estructural. El coste conceptual es que la igualdad por valor deja de ser transparente —la pides con una función en lugar de con ===— pero a cambio es implementable sin reescribir el corazón del motor.

// Propuesta sucesora: Composites (sin sintaxis nueva, sin tocar ===)
const a = Composite({ x: 1, y: 2 });
const b = Composite({ x: 1, y: 2 });

a === b;                 // false: identidad de referencia, como siempre
Composite.equal(a, b);   // true:  igualdad estructural explicita, por valor
📝
Por qué importa la honestidad sobre el estado de una propuesta

En 2026 verás tutoriales que aún presentan Records and Tuples con #{ } como si fueran inminentes. No lo son: la energía se movió a Composites. Saber distinguir la propuesta elegante que no llegó de la propuesta pragmática que avanza es parte de la madurez técnica. La lección de fondo es cuánto cuesta añadir semántica de valor a un lenguaje que nació con semántica de referencia.

Tres inmutabilidades que no hay que confundir

Mientras el lenguaje madura, conviene tener claras las tres capas de inmutabilidad que ya existen, porque operan en momentos distintos y protegen de cosas distintas.

// 1) Tiempo de COMPILACION: TypeScript. Se BORRA al compilar, no existe en runtime.
type Config = Readonly<{ host: string; port: number }>;
const c = { host: "local", port: 80 } as const;

// 2) Tiempo de EJECUCION: real, pero SUPERFICIAL.
const cong = Object.freeze({ a: 1, anidado: { b: 2 } });
cong.anidado.b = 9;   // NO lanza fuera de strict mode: 'anidado' no esta congelado

// 3) Semantica de VALOR: Records o Composites -> profunda MAS igualdad por contenido.

El readonly y el as const de TypeScript son inmutabilidad de compilación: te protegen mientras escribes, pero se borran al transpilar y no impiden nada en tiempo de ejecución. Object.freeze es inmutabilidad de ejecución real, pero superficial: congela un nivel, no el árbol entero. La semántica de valor de Records o Composites sería la única que combina profundidad, garantía en ejecución e igualdad por contenido. En una frase cada una:

  • Compilación, con readonly y as const: protege al escribir, se borra al compilar.
  • Ejecución, con Object.freeze: protege de verdad, pero solo un nivel.
  • Valor, con Records o Composites: profunda e iguala por contenido; aún no en el lenguaje.

La mayoría del código sano combina las dos primeras capas; la tercera es la promesa que el lenguaje todavía debe cumplir.

Ojo con una asimetría que se repite: Readonly<T> en TypeScript también es superficial —solo el primer nivel—; para profundidad hay que componer readonly recursivamente o usar utilidades como DeepReadonly. Otra vez la distinción entre un nivel y el árbol entero, ahora en el sistema de tipos.

💡
Tu pila de inmutabilidad en 2026

Sin esperar a Composites, la combinación práctica hoy es: readonly y as const de TypeScript para atrapar errores al escribir, Immer para las actualizaciones del día a día con structural sharing, Object.freeze selectivo en las fronteras donde un tercero podría mutar por accidente, y librerías persistentes tipadas solo cuando el perfilado señale colecciones enormes. No necesitas el lenguaje para programar inmutable; lo necesitas para dejar de vigilarlo.

Otros lenguajes ya viven ahí

JavaScript llega tarde a una fiesta que otros lenguajes empezaron hace décadas, y mirarlos aclara hacia dónde vamos. El linaje importa: Rich Hickey diseñó las estructuras persistentes de Clojure sobre los hash array mapped tries de Phil Bagwell, y de ahí bebieron Immutable.js y, en espíritu, Immer. Cuando usas structural sharing en JavaScript estás usando una idea con casi dos décadas en producción.

🟢

Clojure

El origen práctico. Sus estructuras persistentes con tries de factor 32 popularizaron el structural sharing; la inmutabilidad es el default y la mutación, la excepción explícita.

🦅

Swift

Los value types, como struct y enum, tienen semántica de valor con copy-on-write en el runtime. Arquitecturas como TCA se apoyan en que copiar el estado es barato y seguro.

🦀

Rust

Inmutable por defecto: una variable es inmutable salvo que declares mut. El sistema de ownership garantiza en compilación que nadie muta lo que otro observa.

🟣

Elixir

Inmutabilidad total heredada de Erlang: ningún dato se muta jamás, lo que vuelve segura por construcción la concurrencia masiva.

// Swift: value semantics. Copiar es barato por copy-on-write del runtime.
struct Estado { var todos: [Todo] }
var a = Estado(todos: [])
var b = a            // copia logica; la fisica solo ocurre si uno muta despues
b.todos.append(t)    // aqui, y solo aqui, se copia el array subyacente
// Rust: inmutable por defecto; 'mut' es una decision explicita y visible.
let estado = vec![1, 2, 3];    // inmutable
let mut editable = estado;      // ownership movido; ahora se puede mutar
editable.push(4);

Ver el mismo problema en varios lenguajes revela que “inmutable por defecto” no es una limitación sino una elección de diseño que descarga trabajo del programador al compilador o al runtime.

flowchart LR
CLJ[Clojure estructuras persistentes] --> IDEA[inmutabilidad por defecto]
SW[Swift value types con copy on write] --> IDEA
RS[Rust inmutable salvo mut] --> IDEA
EL[Elixir inmutabilidad total] --> IDEA
IDEA --> JS[JavaScript de la disciplina a la garantia]
La trayectoria: de la disciplina a la garantía del lenguaje

Todo este track puede leerse como una sola trayectoria: mover la inmutabilidad desde algo que el programador promete hacia algo que el sistema garantiza. Empezó como convención —no mutes, por favor— sostenida solo por la disciplina, frágil ante el primer despiste. Las librerías la reforzaron: Immer la volvió ergonómica, las estructuras persistentes la volvieron barata, el auto-freeze la volvió verificable en desarrollo. El destino natural de esa trayectoria es el lenguaje mismo: que la inmutabilidad y la igualdad por valor sean propiedades del tipo, no rutinas que vigilas. Records and Tuples fue el intento ambicioso de dar ese salto y chocó con la dificultad de reescribir la semántica de un lenguaje adulto; Composites es el intento pragmático que probablemente lo dé, aunque sea pidiendo la igualdad con una función en vez de con un operador. La enseñanza que te llevas trasciende JavaScript: la inmutabilidad no es una moda de un framework, es una dirección de la ingeniería entera, y los lenguajes que la adoptaron pronto —Clojure, Swift, Rust, Elixir— llevan años cosechando lo que aquí apenas empieza a llegar. Cuando programes estado, no estás siguiendo una tendencia pasajera: estás en el lado correcto de una tendencia de fondo de décadas.

⚔️ Sitúa la inmutabilidad en el tiempo
  1. Escribe el mismo cálculo memoizado asumiendo igualdad por referencia y luego imagínalo con igualdad por valor: enumera qué código sobra en el segundo caso.
  2. Demuestra con un ejemplo que Object.freeze es superficial y que as const desaparece en tiempo de ejecución.
  3. Investiga el estado actual de la propuesta Composites en TC39 y resume en qué etapa está y qué la diferencia de Records and Tuples.
  4. Elige uno de los lenguajes de las tarjetas, estudia cómo implementa la inmutabilidad y trae una idea aplicable a tu código JavaScript de hoy.