Skip to content

Toolkit provides two merge helpers:

  • deepMerge(...items): recursively merges nested objects and concatenates arrays.
  • shallowMerge(...items): Object.assign-style top-level merge.

Source Code

View Source Code
ts
import type { Obj } from '../types';

import { isArray } from '../typed/isArray';
import { isObject } from '../typed/isObject';

type DeepMerge<T, U> = T extends Obj
  ? U extends Obj
    ? {
        [K in keyof T | keyof U]: K extends keyof T
          ? K extends keyof U
            ? DeepMerge<T[K], U[K]>
            : T[K]
          : K extends keyof U
            ? U[K]
            : never;
      }
    : U
  : U;

type Merge<T extends Obj[]> = T extends [infer First, ...infer Rest]
  ? First extends Obj
    ? Rest extends Obj[]
      ? DeepMerge<First, Merge<Rest>>
      : First
    : Obj
  : Obj;

function mergeObjects<T extends Obj, U extends Obj>(target: T, source: U): DeepMerge<T, U> {
  if (!isObject(source)) return source as DeepMerge<T, U>;

  const result = { ...target } as DeepMerge<T, U>;

  for (const key of Object.keys(source)) {
    // Guard against prototype pollution: skip dangerous keys
    if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
      continue;
    }

    const sourceValue = source[key];
    const targetValue = result[key];

    (result as any)[key] =
      isArray(sourceValue) && isArray(targetValue)
        ? [...targetValue, ...sourceValue]
        : isObject(sourceValue) && isObject(targetValue)
          ? mergeObjects(targetValue as Obj, sourceValue as Obj)
          : sourceValue;
  }

  return result;
}

/**
 * Deeply merges all provided objects.
 */
export function deepMerge<T extends Obj[]>(...items: [...T]): Merge<T> {
  if (items.length === 0) return {} as Merge<T>;

  return items.reduce((acc, obj) => mergeObjects(acc, obj) as unknown as Merge<T>, {} as Merge<T>);
}

/**
 * Shallowly merges all provided objects.
 */
export function shallowMerge<T extends Obj[]>(...items: [...T]): Merge<T> {
  if (items.length === 0) return {} as Merge<T>;

  return Object.assign({}, ...items) as Merge<T>;
}

Examples

Deep Merge

ts
import { deepMerge } from '@vielzeug/toolkit';

const base = {
  api: { host: 'localhost', port: 8080 },
  features: ['auth'],
};

const override = {
  api: { port: 3000 },
  features: ['metrics'],
};

const merged = deepMerge(base, override);
// {
//   api: { host: 'localhost', port: 3000 },
//   features: ['auth', 'metrics']
// }

Shallow Merge

ts
import { shallowMerge } from '@vielzeug/toolkit';

const base = {
  api: { host: 'localhost', port: 8080 },
  timeout: 1000,
};

const override = {
  api: { port: 3000 },
};

const merged = shallowMerge(base, override);
// {
//   api: { port: 3000 },
//   timeout: 1000
// }

See Also

  • diff: Compare object states.
  • defaults: Fill missing values.
  • path/get: Read merged nested values safely.