Immutable Data Builder
Builder pattern for creating immutable data structures with structural sharing
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 }))
]);