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
| Name | Type | Description |
|---|---|---|
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 mergeisArray(target) && isArray(source)→ Concatenation mergetarget === 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
stableEqualsfor 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