Create immutable state by mutating the current one with structural sharing
—
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Pending
The risk profile of this skill
Configuration options and utility functions for customizing Immer behavior, working with types, and managing immutable state operations.
Controls whether Immer automatically freezes all copies created by produce functions. Freezing prevents accidental mutations of the immutable result.
/**
* Pass true to automatically freeze all copies created by Immer
* @param value - Boolean to enable/disable auto-freezing
* @default true - Always freeze by default, even in production mode
*/
function setAutoFreeze(value: boolean): void;Usage Examples:
import { produce, setAutoFreeze } from "immer";
const baseState = { items: [1, 2, 3], meta: { count: 3 } };
// Default behavior - results are frozen
const result1 = produce(baseState, draft => {
draft.items.push(4);
});
console.log(Object.isFrozen(result1)); // true
console.log(Object.isFrozen(result1.items)); // true
// Attempting to mutate frozen objects throws in strict mode
try {
result1.items.push(5); // TypeError: Cannot add property 3, object is not extensible
} catch (error) {
console.log("Mutation prevented by freezing");
}
// Disable auto-freezing for performance or specific use cases
setAutoFreeze(false);
const result2 = produce(baseState, draft => {
draft.items.push(5);
});
console.log(Object.isFrozen(result2)); // false
console.log(Object.isFrozen(result2.items)); // false
// Now mutation is possible (but not recommended)
result2.items.push(6); // Works, but breaks immutability contract
// Re-enable freezing
setAutoFreeze(true);
const result3 = produce(result2, draft => {
draft.meta.updated = true;
});
console.log(Object.isFrozen(result3)); // trueControls whether Immer uses strict shallow copy mode, which copies object descriptors such as getters, setters, and non-enumerable properties.
/**
* Pass true to enable strict shallow copy
* @param value - Boolean or "class_only" to enable strict copying
* @default false - By default, Immer does not copy object descriptors
*/
function setUseStrictShallowCopy(value: boolean | "class_only"): void;Usage Examples:
import { produce, setUseStrictShallowCopy } from "immer";
// Object with descriptors
const baseState = {};
Object.defineProperty(baseState, 'computed', {
get() { return this._value * 2; },
set(val) { this._value = val; },
enumerable: false
});
Object.defineProperty(baseState, '_value', {
value: 10,
writable: true,
enumerable: false
});
// Default behavior - descriptors are not copied
const result1 = produce(baseState, draft => {
draft.newProp = 'added';
});
console.log(Object.getOwnPropertyDescriptor(result1, 'computed')); // undefined
console.log(result1._value); // undefined
// Enable strict shallow copy
setUseStrictShallowCopy(true);
const result2 = produce(baseState, draft => {
draft.anotherProp = 'also added';
});
console.log(Object.getOwnPropertyDescriptor(result2, 'computed')); // { get: [Function], set: [Function], enumerable: false, ... }
console.log(result2._value); // 10
console.log(result2.computed); // 20
// Class-only mode - only copy descriptors for class instances
setUseStrictShallowCopy("class_only");
class MyClass {
constructor(public value: number) {}
get doubled() {
return this.value * 2;
}
}
const classInstance = new MyClass(5);
const plainObject = { prop: 'value' };
const classResult = produce(classInstance, draft => {
draft.value = 10;
});
const plainResult = produce(plainObject, draft => {
draft.newProp = 'added';
});
// Descriptors copied for class instances but not plain objects in "class_only" mode
console.log(classResult.doubled); // 20 (getter preserved)
console.log(Object.getOwnPropertyDescriptor(plainResult, 'prop')); // Basic descriptor onlyManually freezes draftable objects. This is primarily used internally by Immer but can be useful for manually freezing objects.
/**
* Freezes draftable objects, returns the original object
* @param obj - Object to freeze
* @param deep - If true, freezes recursively (default: false)
* @returns The frozen object
*/
function freeze<T>(obj: T, deep?: boolean): T;Usage Examples:
import { freeze, isDraftable } from "immer";
const mutableObject = {
user: { name: "Alice", age: 30 },
items: [1, 2, 3],
config: { theme: "light" }
};
// Shallow freeze
const shallowFrozen = freeze(mutableObject);
console.log(Object.isFrozen(shallowFrozen)); // true
console.log(Object.isFrozen(shallowFrozen.user)); // false (shallow only)
// Attempting to modify top-level properties fails
try {
shallowFrozen.newProp = 'fails';
} catch (error) {
console.log("Top-level mutation prevented");
}
// But nested objects can still be modified
shallowFrozen.user.age = 31; // Works because user object is not frozen
console.log(shallowFrozen.user.age); // 31
// Deep freeze
const deepFrozen = freeze({
user: { name: "Bob", profile: { bio: "Developer" } },
data: [{ id: 1, value: "test" }]
}, true);
console.log(Object.isFrozen(deepFrozen)); // true
console.log(Object.isFrozen(deepFrozen.user)); // true
console.log(Object.isFrozen(deepFrozen.user.profile)); // true
console.log(Object.isFrozen(deepFrozen.data)); // true
console.log(Object.isFrozen(deepFrozen.data[0])); // true
// All levels are now immutable
try {
deepFrozen.user.name = "Charlie"; // TypeError in strict mode
} catch (error) {
console.log("Deep mutation prevented");
}
// freeze only affects draftable objects
const primitive = 42;
const frozenPrimitive = freeze(primitive);
console.log(frozenPrimitive === primitive); // true (primitives are unchanged)
// Works with arrays
const array = [{ id: 1 }, { id: 2 }];
const frozenArray = freeze(array, true);
console.log(Object.isFrozen(frozenArray)); // true
console.log(Object.isFrozen(frozenArray[0])); // trueType casting utility that tells TypeScript to treat an immutable type as a draft type. This is a no-op at runtime but helps with type safety.
/**
* This function is actually a no-op, but can be used to cast an immutable type
* to a draft type and make TypeScript happy
* @param value - Value to cast
* @returns Same value, typed as Draft<T>
*/
function castDraft<T>(value: T): Draft<T>;Usage Examples:
import { produce, castDraft, Draft, Immutable } from "immer";
// Scenario: Working with immutable data that needs to be treated as draft
function updateUserInPlace<T extends { user: { name: string; age: number } }>(
state: Immutable<T>,
updater: (user: Draft<T['user']>) => void
): T {
return produce(state, draft => {
// TypeScript knows draft.user is already a Draft<T['user']>
updater(draft.user);
});
}
// But sometimes you need to pass immutable data to a function expecting drafts
function processUser(user: Draft<{ name: string; age: number }>) {
user.name = user.name.toUpperCase();
user.age += 1;
}
const immutableState: Immutable<{ user: { name: string; age: number } }> = {
user: { name: "alice", age: 25 }
};
// This would cause TypeScript error without castDraft:
// processUser(immutableState.user); // Error: Immutable<> not assignable to Draft<>
const result = produce(immutableState, draft => {
// Cast immutable user to draft type for function that expects drafts
const userAsDraft = castDraft(immutableState.user);
// Now TypeScript accepts it, even though we're not actually using it
// (in practice, you'd use draft.user directly)
processUser(draft.user);
});
// More practical example: conditional draft processing
function conditionalUpdate<T>(
state: T,
condition: boolean,
updater: (draft: Draft<T>) => void
): T {
if (condition) {
return produce(state, updater);
} else {
// Return original state, but cast to maintain type compatibility
return castDraft(state) as T;
}
}
// Usage in generic contexts
interface Repository<T> {
update(updater: (draft: Draft<T>) => void): T;
getImmutable(): Immutable<T>;
}
class ImmutableRepository<T> implements Repository<T> {
constructor(private data: T) {}
update(updater: (draft: Draft<T>) => void): T {
this.data = produce(this.data, updater);
return this.data;
}
getImmutable(): Immutable<T> {
// Cast to immutable type for type safety
return this.data as Immutable<T>;
}
// Method that needs to work with both draft and immutable versions
processData(processor: (data: Draft<T>) => void): void {
if (this.isDraft(this.data)) {
processor(this.data as Draft<T>);
} else {
processor(castDraft(this.data));
}
}
private isDraft(value: any): boolean {
// In real implementation, would use isDraft from immer
return false; // Simplified for example
}
}Type casting utility that tells TypeScript to treat a mutable type as an immutable type. This is a no-op at runtime but helps with type safety.
/**
* This function is actually a no-op, but can be used to cast a mutable type
* to an immutable type and make TypeScript happy
* @param value - Value to cast
* @returns Same value, typed as Immutable<T>
*/
function castImmutable<T>(value: T): Immutable<T>;Usage Examples:
import { produce, castImmutable, Draft, Immutable } from "immer";
// Scenario: Function that returns data that should be treated as immutable
function createImmutableUser(name: string, age: number): Immutable<{ name: string; age: number }> {
const user = { name, age }; // Mutable object
// Cast to immutable type to enforce immutability contract
return castImmutable(user);
}
// Usage with state management
class StateManager<T> {
private _state: T;
constructor(initialState: T) {
this._state = initialState;
}
// Return state as immutable to prevent external mutations
getState(): Immutable<T> {
return castImmutable(this._state);
}
// Update state and return new immutable version
updateState(updater: (draft: Draft<T>) => void): Immutable<T> {
this._state = produce(this._state, updater);
return castImmutable(this._state);
}
// Unsafe direct access for internal use only
private getMutableState(): T {
return this._state;
}
}
// Using with APIs that expect immutable data
interface ImmutableStore<T> {
data: Immutable<T>;
update(data: Immutable<T>): void;
}
function integrateWithStore<T>(
store: ImmutableStore<T>,
mutableData: T
): void {
// Cast mutable data to immutable for store compatibility
store.update(castImmutable(mutableData));
}
// Type-safe builder pattern
class ImmutableBuilder<T> {
private data: Partial<T> = {};
set<K extends keyof T>(key: K, value: T[K]): this {
this.data[key] = value;
return this;
}
build(): Immutable<T> {
if (!this.isComplete()) {
throw new Error("Incomplete data");
}
// Cast completed mutable data to immutable
return castImmutable(this.data as T);
}
private isComplete(): this is { data: T } {
// Simplified completeness check
return Object.keys(this.data).length > 0;
}
}
// Usage
const immutableUser = new ImmutableBuilder<{ name: string; age: number }>()
.set('name', 'Alice')
.set('age', 30)
.build();
// Now immutableUser has Immutable<> type, preventing accidental mutations
// immutableUser.name = "Bob"; // TypeScript errorFor advanced use cases, you can create custom Immer instances with specific configurations:
import { Immer } from "immer";
// Create custom instance with specific settings
const customImmer = new Immer({
autoFreeze: false,
useStrictShallowCopy: "class_only"
});
// Use custom instance methods
const result = customImmer.produce(baseState, draft => {
draft.modified = true;
});
// Custom instance maintains its own configuration
console.log(Object.isFrozen(result)); // false (autoFreeze disabled)
// You can also create multiple instances with different configs
const debugImmer = new Immer({ autoFreeze: true });
const performanceImmer = new Immer({ autoFreeze: false });
// Use appropriate instance based on context
const debugResult = debugImmer.produce(data, draft => {
// Changes for debugging
});
const productionResult = performanceImmer.produce(data, draft => {
// Performance-critical changes
});import { isDraft, isDraftable, castDraft, castImmutable } from "immer";
// Type-safe utility functions
function safeCastDraft<T>(value: T): Draft<T> | null {
return isDraftable(value) ? castDraft(value) : null;
}
function safeCastImmutable<T>(value: T): Immutable<T> | null {
return isDraftable(value) ? castImmutable(value) : null;
}
// Generic processor that handles both drafts and regular objects
function processAnyValue<T>(
value: T,
processor: (v: Draft<T>) => void
): T {
if (isDraft(value)) {
processor(value as Draft<T>);
return value;
} else if (isDraftable(value)) {
return produce(value, processor);
} else {
// Cannot be drafted, return as-is
return value;
}
}Configuration and utility functions provide fine-grained control over Immer's behavior and help maintain type safety in complex TypeScript applications.