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 asreadonly, 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 constachieves 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 (
constvsletis 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:
forloops are largely redundant if collections have goodmap/forEach; others push back thatforEachis 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/readonlyare 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-tsandeffect-tsare 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.