CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-immer

Create immutable state by mutating the current one with structural sharing

Pending
Quality

Pending

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

SecuritybySnyk

Pending

The risk profile of this skill

Overview
Eval results
Files

configuration-utilities.mddocs/

Configuration & Utilities

Configuration options and utility functions for customizing Immer behavior, working with types, and managing immutable state operations.

Capabilities

setAutoFreeze

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)); // true

setUseStrictShallowCopy

Controls 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 only

freeze

Manually 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])); // true

castDraft

Type 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
  }
}

castImmutable

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 error

Custom Immer Instances

For 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
});

Utility Type Guards

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.

docs

configuration-utilities.md

core-production.md

draft-management.md

index.md

patches-system.md

tile.json