Skip to main content

merge

Performs deep recursive merging of two objects with intelligent type handling. Recursively combines properties from source and target objects, with source properties taking precedence. Handles nested objects, arrays, and mixed structures intelligently by merging compatible types and replacing incompatible ones. Mutates the target object in-place for performance while preserving type safety.

Signature

const merge: <Target extends Record<PropertyKey, any>, Source extends Record<PropertyKey, any>>(target: Target, source: Source) => Target & Source

Parameters

NameTypeDescription
target-Target object to merge into (mutated in-place)
source-Source object to merge from (not modified)

Returns

The mutated target object with merged properties (Target & Source)

Examples

Basic object merging

import { merge } from '@winglet/common-utils';

const target = { a: 1, b: 2, c: { x: 10 } };
const source = { b: 3, c: { y: 20 }, d: 4 };

const result = merge(target, source);
console.log(result); // { a: 1, b: 3, c: { x: 10, y: 20 }, d: 4 }
console.log(target === result); // true (target is mutated)

// Source values override target values
const config1 = { timeout: 1000, retries: 3 };
const config2 = { timeout: 2000, debug: true };
merge(config1, config2);
console.log(config1); // { timeout: 2000, retries: 3, debug: true }

Deep nested object merging

const userDefaults = {
profile: {
personal: { name: '', age: 0 },
settings: { theme: 'light', notifications: true }
},
preferences: {
language: 'en',
timezone: 'UTC'
}
};

const userData = {
profile: {
personal: { name: 'John', email: 'john@example.com' },
settings: { theme: 'dark' }
},
preferences: {
language: 'es'
}
};

const merged = merge(userDefaults, userData);
console.log(merged);
// {
// profile: {
// personal: { name: 'John', age: 0, email: 'john@example.com' },
// settings: { theme: 'dark', notifications: true }
// },
// preferences: {
// language: 'es',
// timezone: 'UTC'
// }
// }

Array merging behavior

// Arrays are merged by concatenation
const target1 = { tags: ['old', 'legacy'] };
const source1 = { tags: ['new', 'modern'] };
merge(target1, source1);
console.log(target1.tags); // ['old', 'legacy', 'new', 'modern']

// Nested arrays in objects
const target2 = {
data: {
numbers: [1, 2],
strings: ['a', 'b']
}
};
const source2 = {
data: {
numbers: [3, 4],
booleans: [true, false]
}
};
merge(target2, source2);
console.log(target2);
// {
// data: {
// numbers: [1, 2, 3, 4],
// strings: ['a', 'b'],
// booleans: [true, false]
// }
// }

// Array replaces non-array
const target3 = { value: 'string' };
const source3 = { value: [1, 2, 3] };
merge(target3, source3);
console.log(target3.value); // [1, 2, 3] (replaced, not merged)

Type compatibility and replacement

// Compatible types are merged
const target = {
config: { port: 3000, host: 'localhost' },
items: [1, 2]
};
const source = {
config: { port: 8080, ssl: true }, // object + object = merge
items: [3, 4] // array + array = merge
};
merge(target, source);
console.log(target);
// {
// config: { port: 8080, host: 'localhost', ssl: true },
// items: [1, 2, 3, 4]
// }

// Incompatible types are replaced
const target2 = {
value: { nested: 'object' },
data: [1, 2, 3]
};
const source2 = {
value: 'primitive string', // object -> string (replace)
data: { key: 'value' } // array -> object (replace)
};
merge(target2, source2);
console.log(target2);
// {
// value: 'primitive string',
// data: { key: 'value' }
// }

Advanced merging scenarios

// Configuration merging with environment overrides
const defaultConfig = {
database: {
host: 'localhost',
port: 5432,
options: { ssl: false, timeout: 30000 }
},
features: ['basic', 'auth'],
logging: {
level: 'info',
outputs: ['console']
}
};

const envConfig = {
database: {
host: 'prod-db.example.com',
options: { ssl: true, pool: { min: 2, max: 10 } }
},
features: ['advanced', 'analytics'],
logging: {
level: 'error',
outputs: ['file', 'remote']
}
};

const finalConfig = merge(defaultConfig, envConfig);
console.log(finalConfig);
// {
// database: {
// host: 'prod-db.example.com',
// port: 5432,
// options: { ssl: true, timeout: 30000, pool: { min: 2, max: 10 } }
// },
// features: ['basic', 'auth', 'advanced', 'analytics'],
// logging: {
// level: 'error',
// outputs: ['console', 'file', 'remote']
// }
// }

Handling undefined values

// undefined in source doesn't override defined target values
const target = { a: 1, b: 'hello', c: true };
const source = { a: 2, b: undefined, d: 'new' };
merge(target, source);
console.log(target); // { a: 2, b: 'hello', c: true, d: 'new' }

// undefined in target gets overridden by defined source values
const target2 = { a: undefined, b: 2 };
const source2 = { a: 'defined', c: 3 };
merge(target2, source2);
console.log(target2); // { a: 'defined', b: 2, c: 3 }

// Both undefined - source wins
const target3 = { a: undefined };
const source3 = { a: undefined };
merge(target3, source3);
console.log(target3); // { a: undefined }

Playground

import { merge } from '@winglet/common-utils';

const target = { a: 1, b: 2, c: { x: 10 } };
const source = { b: 3, c: { y: 20 }, d: 4 };

const result = merge(target, source);
console.log(result); // { a: 1, b: 3, c: { x: 10, y: 20 }, d: 4 }
console.log(target === result); // true (target is mutated)

// Source values override target values
const config1 = { timeout: 1000, retries: 3 };
const config2 = { timeout: 2000, debug: true };
merge(config1, config2);
console.log(config1); // { timeout: 2000, retries: 3, debug: true }

Notes

Merging Strategy:

  • Objects: Deep recursive merge of compatible plain objects
  • Arrays: Concatenation merge preserving order (target + source)
  • Primitives: Source value always replaces target value
  • Mixed Types: Source value replaces target when types incompatible

Type Compatibility Rules:

  • isPlainObject(target) && isPlainObject(source) → Deep merge
  • isArray(target) && isArray(source) → Concatenation merge
  • target === undefined || source !== undefined → Source replaces target
  • Otherwise → Source value replaces target value

Performance Characteristics:

  • Time Complexity: O(n + m) where n is target size, m is source size
  • Space Complexity: O(1) additional space (in-place mutation)
  • Optimization: Direct property assignment with minimal overhead

Mutation Behavior:

  • Target Object: Modified in-place and returned
  • Source Object: Never modified, read-only access
  • Nested Objects: New objects created only when merging arrays with objects
  • Reference Safety: Maintains object references where possible

Circular Reference Handling:

// ⚠️ DANGER: This will cause infinite recursion and stack overflow
const circular1: any = { name: 'obj1' };
const circular2: any = { name: 'obj2' };
circular1.ref = circular2;
circular2.ref = circular1;

// This would crash:
// merge(circular1, circular2); // RangeError: Maximum call stack size exceeded

// Safe alternatives:
const safe1 = { name: 'obj1', data: 'value1' };
const safe2 = { name: 'obj2', data: 'value2' };
const merged = merge(safe1, safe2); // ✅ Works fine

Error Cases and Recovery:

// Type incompatibility scenarios
const target = { value: { nested: 'object' } };
const source = { value: 'primitive' }; // Incompatible type

merge(target, source);
// Result: { value: 'primitive' } - source replaces target

// Property descriptor conflicts
const frozen = {};
Object.defineProperty(frozen, 'key', {
value: 'immutable',
writable: false,
configurable: false
});

try {
merge(frozen, { key: 'new value' });
} catch (error) {
console.error('Cannot assign to read-only property'); // TypeError
}

Edge Cases:

  • Empty objects merge successfully
  • Circular references cause infinite recursion (no built-in protection)
  • Non-enumerable properties are ignored
  • Symbol properties are not processed
  • Prototype properties are not merged
  • Read-only properties may throw TypeError when overridden

Use Cases:

  • Configuration management and environment-specific overrides
  • Default options merging in libraries and frameworks
  • State management and partial updates
  • API response data combination
  • Form data preprocessing and validation setup
  • Theme and styling object composition

Production Considerations:

  • Security: Validate input objects to prevent prototype pollution attacks
  • Performance: Avoid merging very large objects frequently (consider caching)
  • Memory: Monitor memory usage with deep object hierarchies
  • Error Handling: Wrap in try-catch for production environments with user data
  • Circular Reference Detection: Consider using stableEquals for safer alternatives

Alternative Approaches:

  • Object.assign(): Shallow merge only, no deep nesting support
  • { ...target, ...source }: Shallow spread, overwrites nested objects
  • Lodash merge(): Similar functionality but immutable, heavier weight
  • Custom recursive solutions: More control but higher complexity