/deep-merge

Performs a deep merge of two objects, where the source object's properties are merged into the target object.

Primary LanguageTypeScriptMIT LicenseMIT

@hyperse/deep-merge

A TypeScript library for performing deep merges of objects with advanced configuration options. Unlike Object.assign(), this library creates new objects without mutating the original target, and provides fine-grained control over how different types of values are merged.

build stable version GitHub top language Licence

Features

  • Immutable Merging: Target objects are never mutated; new objects are always returned
  • Deep Recursive Merging: Nested objects are merged recursively
  • Type Safety: Full TypeScript support with generic types
  • Class Instance Handling: Special handling for class instances vs plain objects
  • Array Replacement: Arrays are treated as single values and replaced (not merged)
  • Undefined Value Control: Optional merging of undefined values
  • Fast Deep Cloning: Efficient deep cloning utility for simple objects

Installation

npm install @hyperse/deep-merge
# or
yarn add @hyperse/deep-merge
# or
pnpm add @hyperse/deep-merge

API Reference

mergeOptions<T>(target: T, source: DeepPartial<T>, mergeUndefined?: boolean, depth?: number): T

Performs a deep merge of two objects, where the source object's properties are merged into the target object.

Parameters

  • target: T - The target object to merge into
  • source: DeepPartial<T> - The source object to merge from
  • mergeUndefined?: boolean - Whether to merge undefined values (default: false)
  • depth?: number - The current depth of the merge (used internally, default: 0)

Returns

  • T - A new object containing the merged properties

Behavior

  • Objects: Recursively merged if they are plain objects (not class instances)
  • Arrays: Replaced entirely (not merged)
  • Class Instances: Replaced entirely (not merged)
  • Primitives: Replaced by source values
  • Undefined Values: Only merged if mergeUndefined is true

simpleDeepClone<T>(input: T): T

An extremely fast function for deep-cloning objects that contain only simple values (primitives, arrays, and nested simple objects).

Parameters

  • input: T - The input to clone

Returns

  • T - The cloned input

Behavior

  • Primitives: Returned as-is
  • Arrays: Deep cloned recursively
  • Plain Objects: Deep cloned recursively
  • Class Instances: Returned as-is (not cloned)
  • Null/Undefined: Returned as-is

Utility Functions

isObject(item: any): item is object

Checks if an item is a plain object (not an array or null).

isClassInstance(item: any): boolean

Checks if an item is a class instance (has a constructor that's not Object).

Type Definitions

DeepPartial<T>

A utility type that makes all properties of T optional recursively.

Usage Examples

Basic Object Merging

import { mergeOptions } from '@hyperse/deep-merge';

const target = {
  name: 'John',
  age: 30,
  address: {
    city: 'New York',
    country: 'USA',
  },
  hobbies: ['reading', 'swimming'],
};

const source = {
  age: 31,
  address: {
    city: 'Los Angeles',
  },
  hobbies: ['coding'],
};

const result = mergeOptions(target, source);
// Result:
// {
//   name: 'John',
//   age: 31,
//   address: {
//     city: 'Los Angeles',
//     country: 'USA'
//   },
//   hobbies: ['coding']
// }

Plugin Configuration Merging

import { mergeOptions } from '@hyperse/deep-merge';

const defaultConfig = {
  dts: true,
  input: { index: 'src/index.ts' },
  logSilent: false,
  modularImports: [],
  plugin: {
    extraPlugins: [],
    pluginConfigs: {
      multiInputOptions: { fastGlobOptions: { ignore: [] } },
      replaceOptions: { 'process.env.NODE_ENV': '"production"' },
      aliasOptions: { entries: [] },
      nodeResolveOptions: {
        extensions: ['.js', '.ts', '.tsx', '.json', '.vue'],
      },
      jsonOptions: {},
      commonjsOptions: {},
      babelOptions: { usePreset: 'node' },
      terserOptions: true,
    },
  },
  funcs: {
    fn: () => {},
  },
};

const userConfig = {
  dts: {
    entryPointOptions: {
      libraries: { importedLibraries: ['@dimjs/utils'] },
    },
  },
  input: 'src/index.ts',
  plugin: {
    extraPlugins: [],
    pluginConfigs: {
      terserOptions: {
        came: 1,
      },
    },
  },
  output: { format: 'esm' },
};

const result = mergeOptions(defaultConfig, userConfig);

Handling Undefined Values

import { mergeOptions } from '@hyperse/deep-merge';

const target = {
  a: 1,
  b: { c: 2, d: 3 },
  e: 'hello',
};

const source = {
  a: undefined,
  b: { c: undefined, f: 4 },
  e: undefined,
};

// Without merging undefined values (default)
const result1 = mergeOptions(target, source);
// Result: { a: 1, b: { c: 2, d: 3, f: 4 }, e: 'hello' }

// With merging undefined values
const result2 = mergeOptions(target, source, true);
// Result: { a: undefined, b: { c: undefined, d: 3, f: 4 }, e: undefined }

Deep Cloning

import { simpleDeepClone } from '@hyperse/deep-merge';

const original = {
  user: { name: 'John', preferences: { theme: 'dark' } },
  settings: [1, 2, { nested: true }],
};

const clone = simpleDeepClone(original);

// Modifying the original doesn't affect the clone
original.user.name = 'Jane';
original.settings[0] = 999;

console.log(clone.user.name); // 'John'
console.log(clone.settings[0]); // 1

Class Instance Handling

import { mergeOptions } from '@hyperse/deep-merge';

class User {
  constructor(public name: string) {}
}

class Config {
  constructor(public value: number) {}
}

const target = {
  user: new User('John'),
  config: new Config(100),
  data: { x: 1, y: 2 },
};

const source = {
  user: new User('Jane'),
  data: { y: 3, z: 4 },
};

const result = mergeOptions(target, source);
// Class instances are replaced entirely
// result.user is the new User('Jane') instance
// result.data is { x: 1, y: 3, z: 4 }

Important Notes

Array Behavior

Arrays are treated as single values and are completely replaced during merging:

const target = { items: [1, 2, 3] };
const source = { items: [4, 5] };

const result = mergeOptions(target, source);
// result.items is [4, 5], not [1, 2, 3, 4, 5]

Class Instance Behavior

Class instances are not recursively merged - they are replaced entirely:

class MyClass {
  constructor(public value: number) {}
}

const target = { obj: new MyClass(1) };
const source = { obj: new MyClass(2) };

const result = mergeOptions(target, source);
// result.obj is the new MyClass(2) instance

Immutability

The target object is never mutated. A new object is always returned:

const target = { a: 1, b: { c: 2 } };
const source = { b: { c: 3 } };

const result = mergeOptions(target, source);
console.log(target === result); // false
console.log(target.b === result.b); // false

TypeScript Support

The library provides full TypeScript support with generic types:

interface Config {
  name: string;
  settings: {
    theme: string;
    debug: boolean;
  };
  plugins: string[];
}

const defaultConfig: Config = {
  name: 'default',
  settings: { theme: 'light', debug: false },
  plugins: [],
};

const userConfig: DeepPartial<Config> = {
  settings: { theme: 'dark' },
};

const result = mergeOptions(defaultConfig, userConfig);
// result is typed as Config

Performance Considerations

  • mergeOptions creates deep clones of the target object, so it's best suited for configuration merging rather than high-frequency operations
  • simpleDeepClone is optimized for simple objects and is much faster than JSON.parse/stringify methods
  • Class instances are not cloned to preserve their behavior and methods

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

License

This project is licensed under the MIT License - see the LICENSE file for details.