Skip to content

objectenvy / merge

Function: merge()

ts
function merge<T, U>(
   obj1, 
   obj2, 
options?): Merge<T, U>;

Defined in: objectEnvy.ts:890

Recursively merge two configuration objects, with obj2 winning on conflicts.

Type Parameters

Type Parameter
T extends ConfigObject
U extends ConfigObject

Parameters

ParameterTypeDescription
obj1TThe base configuration object.
obj2UThe second configuration object; its keys take precedence over obj1.
optionsMergeOptionsMerge options, including arrayMergeStrategy.

Returns

Merge<T, U>

A new object containing all keys from both inputs, with obj2 values winning conflicts.

Remarks

merge performs a symmetric deep merge: for each key present in obj2, its value overwrites the corresponding key in obj1. Nested objects are merged recursively. Arrays are handled according to options.arrayMergeStrategy:

  • 'replace' (default): obj2's array replaces obj1's array.
  • 'concat': arrays from obj1 and obj2 are joined (obj1 first, then obj2).
  • 'concat-unique': same as concat but duplicate primitive values are removed; object items are deduplicated by deep JSON equality.

The return type is Merge<T, U> (from type-fest), which correctly models obj2 keys shadowing obj1 keys at the type level.

Use When

  • You need to combine two configuration objects where neither is the authoritative "defaults" — e.g., merging a base config with a feature-flag overlay.
  • You're composing multiple partial config slices loaded from different sources.
  • You need array concatenation across config layers (concat or concat-unique).

Avoid When

  • You want one object to be authoritative "defaults" and the other to win — use override() instead.
  • You need to merge more than two objects — chain merge(merge(a, b), c) calls.

Pitfalls

  • NEVER rely on merge() to deep-clone the inputs — BECAUSE nested sub-objects are shallow-copied at each level, so mutations to deeply nested objects in the result affect the originals.
  • NEVER use 'concat-unique' to deduplicate object items if equality matters beyond JSON serialisation — BECAUSE the implementation uses JSON.stringify for comparison, which is order-sensitive and ignores undefined values, Date objects, and prototype methods.
  • NEVER assume merge() handles non-plain objects (Map, Set, Date, class instances) correctly — BECAUSE the function checks typeof === 'object' and recurses, producing incorrect results for these types.

Examples

ts
// Deep merge with obj2 winning on shared keys
import { merge } from 'objectenvy';
const config1 = { port: 3000, log: { level: 'info' } };
const config2 = { log: { path: '/var/log' }, debug: true };
const merged = merge(config1, config2);
// { port: 3000, log: { level: 'info', path: '/var/log' }, debug: true }
ts
// Concatenate arrays from two sources
import { merge } from 'objectenvy';
const config1 = { tags: ['prod', 'v1'] };
const config2 = { tags: ['api'] };
const merged = merge(config1, config2, { arrayMergeStrategy: 'concat' });
// { tags: ['prod', 'v1', 'api'] }
ts
// Deduplicate while merging host lists
import { merge } from 'objectenvy';
const config1 = { hosts: ['localhost', 'example.com'] };
const config2 = { hosts: ['example.com', 'api.example.com'] };
const merged = merge(config1, config2, { arrayMergeStrategy: 'concat-unique' });
// { hosts: ['localhost', 'example.com', 'api.example.com'] }

See

override for defaults-style merging where the second argument wins on missing keys only

Default Value

options.arrayMergeStrategy defaults to 'replace'

Released under the MIT License.