Yevhen Klymentiev
dark
light
console
darkness
y.klymentiev@gmail.com
Reusable Snippets|Practical utility code for everyday use — custom-built and ready to share

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.

TypeScript
Copied!
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 and source.

  • 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

TypeScript
Copied!
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.

Codebase: Utilities -> Objects -> deepMerge | Yevhen Klymentiev