| name | immutability-patterns |
| description | Enforce immutable data patterns across JavaScript, TypeScript, Python, and Go. Use when the user writes code that mutates objects, arrays, or state. Apply whenever modifying data structures, updating state, or reviewing code for mutation bugs. Always prefer immutable patterns over in-place mutation.
|
Immutability Patterns
Create new objects instead of mutating existing ones. Immutability prevents shared-state bugs, makes code easier to reason about, and enables reliable change detection.
When to Use
- Writing any function that transforms data
- Updating React/Vue/Svelte state
- Working with Redux or other state management
- Reviewing code that modifies objects or arrays in place
- Any time
.push(), .splice(), delete, or direct property assignment appears on shared data
Core Patterns
JavaScript/TypeScript: Object Updates
function updateUser(user: User, name: string): User {
user.name = name;
return user;
}
function updateUser(user: User, name: string): User {
return { ...user, name };
}
function updateAddress(user: User, city: string): User {
return {
...user,
address: {
...user.address,
city,
},
};
}
function toggleAdmin(user: User): User {
return {
...user,
role: user.role === "admin" ? "user" : "admin",
};
}
JavaScript/TypeScript: Array Operations
function addItem(items: Item[], item: Item): Item[] {
items.push(item);
return items;
}
const added = [...items, newItem];
const prepended = [newItem, ...items];
const removed = items.filter((item) => item.id !== id);
const updated = items.map((item) =>
item.id === id ? { ...item, name: "new" } : item
);
const inserted = [
...items.slice(0, index),
newItem,
...items.slice(index),
];
TypeScript: Readonly Types
interface Config {
readonly apiUrl: string;
readonly timeout: number;
readonly retries: number;
}
function processItems(items: readonly Item[]): readonly Item[] {
return items.filter((item) => item.active);
}
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};
const routes: Readonly<Record<string, string>> = {
home: "/",
login: "/auth/login",
dashboard: "/app/dashboard",
};
Deep Cloning and Complex Updates
const copy = structuredClone(complexObject);
const config = Object.freeze({
api: "https://api.example.com",
timeout: 5000,
});
import { produce } from "immer";
const nextState = produce(state, (draft) => {
draft.users[0].address.city = "New York";
draft.items.push(newItem);
});
Python: Immutable Patterns
from dataclasses import dataclass, replace
from typing import NamedTuple
@dataclass(frozen=True)
class User:
name: str
email: str
role: str = "user"
user = User(name="Alice", email="alice@example.com")
updated = replace(user, role="admin")
ALLOWED_ROLES = ("admin", "user", "viewer")
VALID_STATUSES = frozenset({"active", "inactive", "pending"})
class Point(NamedTuple):
x: float
y: float
original = {"a": 1, "b": 2}
updated = {**original, "b": 3, "c": 4}
Go: Immutable Patterns
type User struct {
Name string
Email string
Role string
}
func (u *User) SetRole(role string) {
u.Role = role
}
func (u User) WithRole(role string) User {
u.Role = role
return u
}
func appendItem(items []Item, item Item) []Item {
result := make([]Item, len(items)+1)
copy(result, items)
result[len(items)] = item
return result
}
func removeItem(items []Item, index int) []Item {
result := make([]Item, 0, len(items)-1)
result = append(result, items[:index]...)
result = append(result, items[index+1:]...)
return result
}
Anti-Patterns
What NOT to Do
- Direct property assignment on shared objects:
user.name = "new" — always spread into a new object.
- Array mutators on shared arrays:
.push(), .pop(), .splice(), .sort(), .reverse() — use immutable alternatives or call on a copy.
- Assuming Object.freeze is deep: It only freezes the top level. Nested objects are still mutable.
- Over-cloning: Do not
structuredClone on every operation — use spread for shallow updates, deep clone only when needed.
const sorted = items.sort((a, b) => a.name.localeCompare(b.name));
const sorted = items.toSorted((a, b) => a.name.localeCompare(b.name));
const sorted = [...items].sort((a, b) => a.name.localeCompare(b.name));
const reversed = items.reverse();
const reversed = items.toReversed();
const reversed = [...items].reverse();
Quick Reference
JavaScript/TypeScript Immutable Operations:
Object update: { ...obj, key: value }
Array append: [...arr, item]
Array remove: arr.filter(x => x.id !== id)
Array update: arr.map(x => x.id === id ? { ...x, ...changes } : x)
Array sort: arr.toSorted(fn) or [...arr].sort(fn)
Array reverse: arr.toReversed() or [...arr].reverse()
Deep clone: structuredClone(obj)
Nested update: immer produce()
TypeScript Types:
readonly prop Readonly<T> ReadonlyArray<T> as const
Python:
@dataclass(frozen=True) replace(obj, field=val)
tuple() frozenset() {**dict, key: val}
Go:
Value receivers Return new structs copy() for slices
Mutating Methods to AVOID (on shared data):
.push() .pop() .shift() .unshift() .splice()
.sort() .reverse() .fill() delete obj.key