Experiment: Making TypeScript immutable-by-default

Mechanisms for immutability in TypeScript/JS

  • Several comments suggest using a TypeScript compiler plugin (e.g. via ts-patch) to add a preprocessing step that rewrites object types as readonly, enforcing immutability by default at type-check time.
  • Others point out existing tools:
    • Object.freeze() plus TypeScript’s typings gives compile‑time errors on mutation; as const achieves similar behavior without runtime calls.
    • Critique: these are opt‑in and usually shallow; they don’t satisfy the “immutable by default” goal and don’t prevent all object mutations.
  • There’s interest in using property setter tricks and conditional types, but skepticism that current TS primitives (object, {}) are flexible enough to redefine default behavior.
  • Some rely on runtime deep cloning (e.g. structuredClone / JSON.parse(JSON.stringify(...))), but this is acknowledged as slow and partial.

Loops, variables, and style

  • Clarification: the experiment targets immutable objects, not banning variable reassignment (const vs let is mostly solved already).
  • For loops in an immutable style, commenters recommend for..of, map/filter/reduce, entries() and higher‑order functions; traditional index‑mutation loops are seen as less suitable.
  • One view: for loops are largely redundant if collections have good map/forEach; others push back that forEach is not meaningfully “more functional” and control flow differences matter.

Alternative languages vs tightening TypeScript

  • Some argue it’s simpler to choose a language that’s immutable‑first or compiles to JS with strong guarantees (Gleam, ReScript/Reason, Scala.js, ClojureScript, Elm, etc.).
  • Counterpoint: TypeScript’s ecosystem, JS interop, hiring pool, and gradual‑adoption story make “stricter TS” more realistic for most teams than a wholesale language switch.

Immutability: benefits, costs, and performance

  • Strong pro‑immutability camp: easier reasoning, safer concurrency, better state management and testing, fewer classes of bugs; default immutability in languages like Clojure/Haskell is described as a “superpower.”
  • Skeptical camp: in JS/TS, immutability is bolted on, often via cloning and spread, which can hurt performance (more allocations, GC pressure, O(N²+) patterns when chaining map/filter).
  • One detailed account from a large TS codebase notes real production regressions from Redux‑style cloning of large state trees; argues that in JS, immutability vs performance is a genuine trade‑off, not a free win.
  • Others respond that mutation’s only advantage is performance; ideally runtimes should make persistent immutable structures fast so the trade‑off mostly disappears, but acknowledge that JS doesn’t have this natively today.

Persistent data structures and equality

  • Multiple comments stress that “effective immutability” requires persistent data structures with structural sharing; otherwise naive copying will “grind to a halt.”
  • Comparisons are made to Clojure’s and Immutable.js’s persistent collections; JS’s freeze/seal/readonly are framed as shallow, local restrictions, not full structural immutability.
  • For full benefits (e.g. cheap equality checks, React optimizations), commenters want value‑based equality and language‑level constructs like the abandoned Records & Tuples proposal or the newer Composites proposal.
  • In the TS world, libraries like fp-ts and effect-ts are cited as ecosystems that try to bring persistent and functional patterns, though they add complexity and are seen by some as “bolt‑ons.”

Terminology and ergonomics

  • Some prefer “read‑write/read‑only” over “mutable/immutable,” but others argue those terms conflate capability with access permissions; immutability implies no one can change the value, not just “you can’t.”
  • A few TS users note that pervasive readonly/deep‑readonly types tend to “infect” a codebase, requiring lots of annotations and boilerplate, which is exactly what an immutable‑by‑default mode aims to reduce.