home / skills / yanko-belov / code-craft / immutability
This skill enforces immutable patterns by returning new objects for updates, preventing bugs and improving React state predictability.
npx playbooks add skill yanko-belov/code-craft --skill immutabilityReview the files below or copy the command above to add this skill to your agents.
---
name: immutability
description: Use when modifying objects or arrays. Use when tempted to mutate function parameters. Use when state changes cause unexpected bugs.
---
# Immutability
## Overview
**Never mutate. Always return new objects.**
Mutation causes bugs that are hard to track - when objects change unexpectedly, debugging becomes a nightmare. Immutability makes state changes explicit and predictable.
## When to Use
- Modifying objects or arrays
- Updating state in React/Redux
- Function receives object as parameter
- Data shared between multiple parts of code
## The Iron Rule
```
NEVER mutate objects or arrays. ALWAYS return new copies with changes.
```
**No exceptions:**
- Not for "it's more efficient"
- Not for "it's simpler"
- Not for "it's just one property"
- Not for "no one else uses this object"
## Detection: Mutation Smell
If you're modifying an object directly, STOP:
```typescript
// ❌ VIOLATION: Direct mutation
function updateUserAddress(user: User, newCity: string): User {
user.address.city = newCity; // Mutates original!
return user;
}
// ❌ VIOLATION: Array mutation
function addItem(cart: CartItem[], item: CartItem): CartItem[] {
cart.push(item); // Mutates original!
return cart;
}
// ❌ VIOLATION: Nested mutation
function updateSettings(config: Config): Config {
config.settings.theme = 'dark'; // Deep mutation!
return config;
}
```
Problems:
- Original object changed unexpectedly
- React won't detect changes (same reference)
- Debugging nightmare - "who changed this?"
- Undo/redo impossible
## The Correct Pattern: Return New Objects
```typescript
// ✅ CORRECT: Return new object
function updateUserAddress(user: User, newCity: string): User {
return {
...user,
address: {
...user.address,
city: newCity
}
};
}
// ✅ CORRECT: Return new array
function addItem(cart: CartItem[], item: CartItem): CartItem[] {
return [...cart, item];
}
// ✅ CORRECT: Deep immutable update
function updateSettings(config: Config): Config {
return {
...config,
settings: {
...config.settings,
theme: 'dark'
}
};
}
// Usage - originals unchanged
const user = { name: 'Alice', address: { city: 'Boston' } };
const updatedUser = updateUserAddress(user, 'Cambridge');
console.log(user.address.city); // 'Boston' - unchanged!
console.log(updatedUser.address.city); // 'Cambridge' - new object
```
## Immutable Operations
### Objects
```typescript
// Update property
const updated = { ...obj, property: newValue };
// Remove property
const { removed, ...rest } = obj;
// Merge objects
const merged = { ...obj1, ...obj2 };
```
### Arrays
```typescript
// Add item
const added = [...arr, newItem];
const prepended = [newItem, ...arr];
// Remove item
const removed = arr.filter(item => item.id !== idToRemove);
// Update item
const updated = arr.map(item =>
item.id === id ? { ...item, ...changes } : item
);
// Sort (creates new array)
const sorted = [...arr].sort((a, b) => a.value - b.value);
```
### Nested Updates
```typescript
// Deep update helper
const updated = {
...state,
users: {
...state.users,
[userId]: {
...state.users[userId],
name: newName
}
}
};
// Or use immer for complex updates
import { produce } from 'immer';
const updated = produce(state, draft => {
draft.users[userId].name = newName; // Looks mutable, but isn't
});
```
## Pressure Resistance Protocol
### 1. "It's More Efficient"
**Pressure:** "Creating new objects is wasteful"
**Response:** Modern JS engines optimize this. The bugs from mutation cost more than the memory.
**Action:** Use immutable patterns. Profile if you suspect performance issues.
### 2. "It's Simpler"
**Pressure:** "Direct mutation is easier to read"
**Response:** Simple to write, hard to debug. Immutability is simpler in the long run.
**Action:** Spread operators are not complex. Use them.
### 3. "It's Just One Property"
**Pressure:** "I'm only changing one field"
**Response:** One mutation sets a precedent. Others follow. Bugs multiply.
**Action:** All updates return new objects. No exceptions.
### 4. "No One Else Uses This"
**Pressure:** "This object is local, mutation is safe"
**Response:** Code evolves. Local becomes shared. Build the habit now.
**Action:** Always immutable, regardless of current usage.
## Red Flags - STOP and Reconsider
- `obj.property = value` (direct assignment)
- `array.push()`, `array.pop()`, `array.splice()`
- `array.sort()` without spreading first
- Modifying function parameters
- `delete obj.property`
**All of these mean: Rewrite immutably.**
## Quick Reference
| Mutable (Bad) | Immutable (Good) |
|---------------|------------------|
| `obj.x = y` | `{ ...obj, x: y }` |
| `arr.push(x)` | `[...arr, x]` |
| `arr.pop()` | `arr.slice(0, -1)` |
| `arr.splice(i, 1)` | `arr.filter((_, idx) => idx !== i)` |
| `arr.sort()` | `[...arr].sort()` |
| `delete obj.x` | `const { x, ...rest } = obj` |
## Common Rationalizations (All Invalid)
| Excuse | Reality |
|--------|---------|
| "More efficient" | Bugs cost more than memory. |
| "Simpler" | Simpler to write, harder to debug. |
| "Just one property" | One exception leads to many. |
| "No one else uses it" | Code changes. Be consistent. |
| "Too verbose" | Spread syntax is concise. |
| "React handles it" | React needs new references to detect changes. |
## The Bottom Line
**Never mutate. Spread and return new objects.**
Every modification returns a new object. Original data stays unchanged. This enables debugging, undo/redo, React change detection, and sanity.
This skill enforces immutable update patterns for objects and arrays in TypeScript code. It guides developers to never mutate function parameters or shared state and to always return new objects, making changes explicit and predictable. The result is fewer subtle bugs, reliable React/Redux state updates, and easier debugging and undo/redo flows.
It inspects code paths where objects or arrays are modified and flags direct mutations (assignments, push/pop/splice, sort without copying, delete, etc.). It recommends and demonstrates immutable alternatives using spread, filter, map, slice, and deep-copy patterns, and points to helpers like immer for complex nested updates. It emphasizes returning new references so change detection and reasoning remain correct.
Why insist on immutability if mutation is sometimes simpler?
Direct mutation may be simpler in the short term but creates hidden coupling and unpredictable bugs. Immutability makes intent explicit, simplifies reasoning, and prevents regressions as code grows.
Won't creating new objects hurt performance?
Modern JS engines are optimized for creating short-lived objects; the cost is usually negligible versus the debugging and correctness cost. Profile only if you see concrete performance issues.