TYPESCRIPT

Immutable Data Builder

Builder pattern for creating immutable data structures with structural sharing

TypeScriptImmutableBuilderFunctional

Code

// Base immutable type
type Immutable<T> = T extends Function ? T : T extends object ? { readonly [K in keyof T]: Immutable<T[K]> } : T;

// Utility type for mutable version
type Mutable<T> = T extends Function ? T : T extends object ? { -readonly [K in keyof T]: Mutable<T[K]> } : T;

class ImmutableBuilder<T extends object> {
  private data: Mutable<T>;
  
  constructor(initialData: T) {
    // Deep clone to ensure immutability
    this.data = this.deepClone(initialData);
  }
  
  // Create a new builder from existing data
  static from<T extends object>(data: T): ImmutableBuilder<T> {
    return new ImmutableBuilder(data);
  }
  
  // Set a value at a specific path
  set<K extends keyof T>(key: K, value: Immutable<T[K]>): ImmutableBuilder<T> {
    const newData = this.cloneWithChange(key, value);
    return new ImmutableBuilder(newData as T);
  }
  
  // Update a value using a transformation function
  update<K extends keyof T>(
    key: K,
    updater: (value: Immutable<T[K]>) => Immutable<T[K]>
  ): ImmutableBuilder<T> {
    const currentValue = this.data[key];
    const newValue = updater(currentValue as Immutable<T[K]>);
    return this.set(key, newValue);
  }
  
  // Merge with another object
  merge(partial: Partial<T>): ImmutableBuilder<T> {
    const newData = { ...this.data, ...partial } as Mutable<T>;
    return new ImmutableBuilder(newData as T);
  }
  
  // Deep merge
  deepMerge(partial: Partial<Mutable<T>>): ImmutableBuilder<T> {
    const newData = this.deepMergeObjects(this.data, partial);
    return new ImmutableBuilder(newData as T);
  }
  
  // Delete a property
  delete<K extends keyof T>(key: K): ImmutableBuilder<Omit<T, K>> {
    const newData = { ...this.data };
    delete newData[key];
    return new ImmutableBuilder(newData as Omit<T, K>);
  }
  
  // Modify nested properties using a lens-like pattern
  lens<K1 extends keyof T, K2 extends keyof T[K1]>(
    key1: K1,
    key2: K2
  ): {
    set: (value: Immutable<T[K1][K2]>) => ImmutableBuilder<T>;
    update: (updater: (value: Immutable<T[K1][K2]>) => Immutable<T[K1][K2]>) => ImmutableBuilder<T>;
  } {
    return {
      set: (value) => {
        const nested = { ...this.data[key1] as object, [key2]: value } as T[K1];
        return this.set(key1, nested);
      },
      update: (updater) => {
        const current = this.data[key1]?.[key2];
        const updated = updater(current as Immutable<T[K1][K2]>);
        return this.lens(key1, key2).set(updated);
      }
    };
  }
  
  // Build the immutable object
  build(): Immutable<T> {
    return this.deepFreeze(this.data) as Immutable<T>;
  }
  
  // Get current mutable data (for inspection only)
  peek(): T {
    return this.data as T;
  }
  
  // Clone with structural sharing
  private cloneWithChange<K extends keyof T>(
    key: K,
    value: Immutable<T[K]>
  ): Mutable<T> {
    return {
      ...this.data,
      [key]: this.isObject(value) ? this.deepClone(value) : value
    } as Mutable<T>;
  }
  
  // Deep clone utility
  private deepClone<T>(obj: T): Mutable<T> {
    __TOKEN_158__ (obj === null || typeof obj !== 'object') {
      return obj as Mutable<T>;
    }
    
    __TOKEN_161__ (Array.isArray(obj)) {
      return obj.map(item => this.deepClone(item)) as Mutable<T>;
    }
    
    const cloned: any = {};
    __TOKEN_165__ (const key in obj) {
      __TOKEN_168__ (Object.prototype.hasOwnProperty.call(obj, key)) {
        cloned[key] = this.deepClone(obj[key]);
      }
    }
    
    return cloned as Mutable<T>;
  }
  
  // Deep merge utility
  private deepMergeObjects<T extends object, U extends Partial<Mutable<T>>>(
    target: T,
    source: U
  ): Mutable<T> {
    const result = { ...target } as any;
    
    __TOKEN_175__ (const key in source) {
      __TOKEN_178__ (source[key] !== undefined) {
        __TOKEN_179__ (this.isObject(source[key]) && this.isObject(result[key])) {
          result[key] = this.deepMergeObjects(result[key], source[key] as any);
        } else __TOKEN_184__ (Array.isArray(source[key]) && Array.isArray(result[key])) {
          result[key] = [...result[key], ...(source[key] as any)];
        } else {
          result[key] = this.isObject(source[key]) 
            ? this.deepClone(source[key])
            : source[key];
        }
      }
    }
    
    return result as Mutable<T>;
  }
  
  // Deep freeze utility
  private deepFreeze<T>(obj: T): Immutable<T> {
    __TOKEN_190__ (obj === null || typeof obj !== 'object') {
      return obj as Immutable<T>;
    }
    
    // Prevent this function from being frozen
    __TOKEN_193__ (Object.isFrozen(obj)) {
      return obj as Immutable<T>;
    }
    
    Object.freeze(obj);
    
    // Recursively freeze all properties
    __TOKEN_195__ (const key in obj) {
      __TOKEN_198__ (Object.prototype.hasOwnProperty.call(obj, key)) {
        const value = obj[key];
        __TOKEN_200__ (typeof value === 'object' && value !== null) {
          this.deepFreeze(value);
        }
      }
    }
    
    return obj as Immutable<T>;
  }
  
  private isObject(value: any): value is object {
    return value !== null && typeof value === 'object';
  }
}

// Record-based immutable data structure
class ImmutableRecord<T extends Record<string, any>> {
  private data: Immutable<T>;
  
  private constructor(data: T) {
    this.data = new ImmutableBuilder(data).build();
  }
  
  // Factory methods
  static create<T extends Record<string, any>>(initialData: T): ImmutableRecord<T> {
    return new ImmutableRecord(initialData);
  }
  
  static empty<T extends Record<string, any>>(): ImmutableRecord<T> {
    return new ImmutableRecord({} as T);
  }
  
  // Get value
  get<K extends keyof T>(key: K): Immutable<T[K]> {
    return this.data[key];
  }
  
  // Check if has key
  has<K extends keyof T>(key: K): boolean {
    return key in this.data;
  }
  
  // Set value (returns new record)
  set<K extends keyof T>(key: K, value: T[K]): ImmutableRecord<T> {
    const builder = new ImmutableBuilder(this.data as T);
    const newData = builder.set(key, value).build();
    return new ImmutableRecord(newData as T);
  }
  
  // Update value (returns new record)
  update<K extends keyof T>(
    key: K,
    updater: (value: Immutable<T[K]>) => T[K]
  ): ImmutableRecord<T> {
    const current = this.get(key);
    const updated = updater(current);
    return this.set(key, updated);
  }
  
  // Merge with another record
  merge(other: Partial<T>): ImmutableRecord<T> {
    const builder = new ImmutableBuilder(this.data as T);
    const newData = builder.merge(other).build();
    return new ImmutableRecord(newData as T);
  }
  
  // Delete a key
  delete<K extends keyof T>(key: K): ImmutableRecord<Omit<T, K>> {
    const builder = new ImmutableBuilder(this.data as T);
    const newData = builder.delete(key).build();
    return new ImmutableRecord(newData as Omit<T, K>);
  }
  
  // Map over entries
  map<U>(fn: (value: Immutable<T[keyof T]>, key: keyof T) => U): U[] {
    return Object.entries(this.data).map(([key, value]) => 
      fn(value as Immutable<T[keyof T]>, key as keyof T)
    );
  }
  
  // Filter entries
  filter(predicate: (value: Immutable<T[keyof T]>, key: keyof T) => boolean): Partial<T> {
    const result: Partial<T> = {};
    
    __TOKEN_257__ (const key in this.data) {
      const value = this.data[key];
      __TOKEN_263__ (predicate(value, key as keyof T)) {
        result[key] = value;
      }
    }
    
    return result;
  }
  
  // Reduce over entries
  reduce<U>(
    fn: (acc: U, value: Immutable<T[keyof T]>, key: keyof T) => U,
    initialValue: U
  ): U {
    let acc = initialValue;
    
    __TOKEN_266__ (const key in this.data) {
      const value = this.data[key];
      acc = fn(acc, value, key as keyof T);
    }
    
    return acc;
  }
  
  // Convert to plain object
  toObject(): Immutable<T> {
    return this.data;
  }
  
  // Get keys
  keys(): (keyof T)[] {
    return Object.keys(this.data) as (keyof T)[];
  }
  
  // Get values
  values(): Immutable<T[keyof T]>[] {
    return Object.values(this.data);
  }
  
  // Get entries
  entries(): [keyof T, Immutable<T[keyof T]>][] {
    return Object.entries(this.data) as [keyof T, Immutable<T[keyof T]>][];
  }
  
  // Get size
  size(): number {
    return this.keys().length;
  }
}

// Usage examples
interface User {
  id: number;
  name: string;
  email: string;
  profile: {
    age: number;
    address: {
      street: string;
      city: string;
    };
  };
  preferences: {
    theme: 'light' | 'dark';
    notifications: boolean;
  };
}

// Create immutable user
const userBuilder = new ImmutableBuilder<User>({
  id: 1,
  name: 'John Doe',
  email: '[email protected]',
  profile: {
    age: 30,
    address: {
      street: '123 Main St',
      city: 'New York'
    }
  },
  preferences: {
    theme: 'light',
    notifications: true
  }
});

// Apply transformations
const updatedUser = userBuilder
  .set('name', 'John Smith')
  .update('profile', profile => ({
    ...profile,
    age: 31
  }))
  .lens('profile', 'address')
    .set({ street: '456 Oak Ave', city: 'Boston' })
  .update('preferences', prefs => ({
    ...prefs,
    theme: 'dark'
  }))
  .build();

console.log(updatedUser.name); // 'John Smith'
console.log(updatedUser.profile.age); // 31

// Try to mutate (will fail in strict mode)
// updatedUser.name = 'Jane'; // Error: Cannot assign to read-only property

// Using ImmutableRecord
const userRecord = ImmutableRecord.create({
  id: 1,
  name: 'Alice',
  active: true
});

const updatedRecord = userRecord
  .set('name', 'Alice Smith')
  .set('active', false)
  .merge({ email: '[email protected]' });

console.log(updatedRecord.get('name')); // 'Alice Smith'
console.log(updatedRecord.has('email')); // true

// Complex transformation with structural sharing
const data = {
  users: {
    '1': { name: 'John', age: 30 },
    '2': { name: 'Jane', age: 25 }
  },
  metadata: {
    count: 2,
    timestamp: Date.now()
  }
};

const builder = new ImmutableBuilder(data);
const updated = builder
  .lens('users', '1')
    .set({ name: 'Jonathan', age: 31 })
  .update('metadata', meta => ({
    ...meta,
    count: 3,
    updated: true
  }))
  .build();

console.log(updated.users['1'].name); // 'Jonathan'
console.log(data.users['1'].name); // 'John' (original unchanged)

// Batch updates
function batchUpdate<T extends object>(
  data: T,
  updates: Array<(builder: ImmutableBuilder<T>) => ImmutableBuilder<T>>
): Immutable<T> {
  let builder = new ImmutableBuilder(data);
  __TOKEN_297__ (const update of updates) {
    builder = update(builder);
  }
  return builder.build();
}

const batchUpdated = batchUpdate(data, [
  builder => builder.lens('users', '1').set({ name: 'Updated', age: 99 }),
  builder => builder.update('metadata', meta => ({ ...meta, batch: true }))
]);