deepMerge
Recursively deep merges two objects. For properties that exist in both objects: if both values are plain objects, they are merged recursively, otherwise, the source value overwrites the target value.
1/**
2 * Recursively deep merges two objects.
3 * For properties that exist in both objects:
4 * - If both values are plain objects, they are merged recursively.
5 * - Otherwise, the source value overwrites the target value.
6 *
7 * @param target - The base object.
8 * @param source - The object with properties to merge in.
9 * @returns A new object with merged properties.
10 */
11export function deepMerge<T extends object, U extends object>(target: T, source: U): T & U {
12 const result = { ...target } as T & U;
13
14 for (const key in source) {
15 const sourceVal = source[key];
16 const targetVal = target[key];
17
18 if (
19 isPlainObject(sourceVal) &&
20 isPlainObject(targetVal)
21 ) {
22 result[key] = deepMerge(targetVal as any, sourceVal as any);
23 } else {
24 result[key] = sourceVal as any;
25 }
26 }
27
28 return result;
29}
30
31function isPlainObject(value: unknown): value is Record<string, any> {
32 return (
33 typeof value === 'object' &&
34 value !== null &&
35 value.constructor === Object
36 );
37}
Recursive Deep Merging
Merges nested plain objects instead of just overwriting them, allowing for granular property-level updates.
Non-Mutative Design
Returns a new object without modifying the input objects, preserving immutability.
Type-Safe Composition
Uses TypeScript generics to accurately infer and combine the types of
target
andsource
.Handles Edge Cases Cleanly
Differentiates between plain objects and other types (like arrays, classes, or special objects) for safe merging.
Readable and Maintainable
Implements merging logic with clear and concise structure, avoiding complex third-party dependencies.
Tests | Examples
1test('deepMerge - merges flat objects', () => {
2 const a = { x: 1, y: 2 };
3 const b = { y: 3, z: 4 };
4 expect(deepMerge(a, b)).toEqual({ x: 1, y: 3, z: 4 });
5});
6
7test('deepMerge - merges nested objects', () => {
8 const a = { foo: { bar: 1 } };
9 const b = { foo: { baz: 2 } };
10 expect(deepMerge(a, b)).toEqual({ foo: { bar: 1, baz: 2 } });
11});
12
13test('deepMerge - source overwrites non-object in target', () => {
14 const a = { val: 1 };
15 const b = { val: { deep: true } };
16 expect(deepMerge(a, b)).toEqual({ val: { deep: true } });
17});
18
19test('deepMerge - handles arrays by overwriting', () => {
20 const a = { items: [1, 2, 3] };
21 const b = { items: [4, 5] };
22 expect(deepMerge(a, b)).toEqual({ items: [4, 5] });
23});
24
25test('deepMerge - preserves target when source is empty', () => {
26 const a = { a: 1, b: { c: 2 } };
27 const b = {};
28 expect(deepMerge(a, b)).toEqual({ a: 1, b: { c: 2 } });
29});
30
31test('deepMerge - preserves source when target is empty', () => {
32 const a = {};
33 const b = { a: 1, b: { c: 2 } };
34 expect(deepMerge(a, b)).toEqual({ a: 1, b: { c: 2 } });
35});
Common Use Cases
Configuration Merging
Combine default settings with user-defined overrides in CLI tools, web apps, or libraries.
Form State Updates
Merge form input values into existing state trees without losing untouched nested fields.
Dynamic Theme or Style Extension
Compose themes or UI styles by layering design tokens or partial overrides.
Localization or Content Trees
Merge language packs or content modules while preserving existing keys.
Plugin or Middleware Composition
Integrate user-provided options with core system configurations in extensible systems.