immutability-patterns — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited immutability-patterns (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Create new objects instead of mutating existing ones. Immutability prevents shared-state bugs, makes code easier to reason about, and enables reliable change detection.
.push(), .splice(), delete, or direct property assignment appears on shared data// WRONG: Mutation
function updateUser(user: User, name: string): User {
user.name = name; // Mutates the original!
return user;
}
// CORRECT: Spread operator
function updateUser(user: User, name: string): User {
return { ...user, name };
}
// CORRECT: Nested object update
function updateAddress(user: User, city: string): User {
return {
...user,
address: {
...user.address,
city,
},
};
}
// CORRECT: Conditional field update
function toggleAdmin(user: User): User {
return {
...user,
role: user.role === "admin" ? "user" : "admin",
};
}// WRONG: Mutating arrays
function addItem(items: Item[], item: Item): Item[] {
items.push(item); // Mutates!
return items;
}
// CORRECT: Immutable array operations
const added = [...items, newItem]; // append
const prepended = [newItem, ...items]; // prepend
const removed = items.filter((item) => item.id !== id); // remove
const updated = items.map((item) => // update
item.id === id ? { ...item, name: "new" } : item
);
const inserted = [ // insert at index
...items.slice(0, index),
newItem,
...items.slice(index),
];// Mark data as immutable at the type level
interface Config {
readonly apiUrl: string;
readonly timeout: number;
readonly retries: number;
}
// Readonly arrays
function processItems(items: readonly Item[]): readonly Item[] {
// items.push(x) -> TypeScript error!
return items.filter((item) => item.active);
}
// Deep readonly utility
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};
// Readonly records
const routes: Readonly<Record<string, string>> = {
home: "/",
login: "/auth/login",
dashboard: "/app/dashboard",
};// structuredClone for deep copies (no functions or symbols)
const copy = structuredClone(complexObject);
// Object.freeze for shallow immutability (runtime enforcement)
const config = Object.freeze({
api: "https://api.example.com",
timeout: 5000,
});
// config.timeout = 3000; -> throws in strict mode, silent in sloppy mode
// For complex nested updates, use a library like immer
import { produce } from "immer";
const nextState = produce(state, (draft) => {
draft.users[0].address.city = "New York"; // Looks like mutation
draft.items.push(newItem); // but produces a new object
});from dataclasses import dataclass, replace
from typing import NamedTuple
# Use frozen dataclasses
@dataclass(frozen=True)
class User:
name: str
email: str
role: str = "user"
# Create updated copy with replace()
user = User(name="Alice", email="[email protected]")
updated = replace(user, role="admin") # New object, original unchanged
# Use tuples instead of lists for fixed collections
ALLOWED_ROLES = ("admin", "user", "viewer")
# Use frozenset for immutable sets
VALID_STATUSES = frozenset({"active", "inactive", "pending"})
# NamedTuple for immutable records
class Point(NamedTuple):
x: float
y: float
# Dict updates without mutation
original = {"a": 1, "b": 2}
updated = {**original, "b": 3, "c": 4} # New dict// Return new structs instead of modifying pointers
type User struct {
Name string
Email string
Role string
}
// WRONG: Mutation via pointer
func (u *User) SetRole(role string) {
u.Role = role
}
// CORRECT: Return new value
func (u User) WithRole(role string) User {
u.Role = role // Modifying the copy (value receiver)
return u
}
// Immutable slice operations
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
}user.name = "new" — always spread into a new object..push(), .pop(), .splice(), .sort(), .reverse() — use immutable alternatives or call on a copy.structuredClone on every operation — use spread for shallow updates, deep clone only when needed.// BAD: Sorting mutates the original array
const sorted = items.sort((a, b) => a.name.localeCompare(b.name));
// GOOD: toSorted (ES2023) or spread + sort
const sorted = items.toSorted((a, b) => a.name.localeCompare(b.name));
const sorted = [...items].sort((a, b) => a.name.localeCompare(b.name));
// BAD: Reversing mutates
const reversed = items.reverse();
// GOOD: toReversed (ES2023) or spread + reverse
const reversed = items.toReversed();
const reversed = [...items].reverse();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~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.