merge
The merge utility combines multiple objects into a single new object using a variety of configurable strategies.
Implementation
View Source Code
ts
import { isArray } from '../typed/isArray';
import { isObject } from '../typed/isObject';
import type { Obj } from '../types';
// #region MergeStrategy
type MergeStrategy =
| 'deep'
| 'shallow'
| 'lastWins'
| 'arrayConcat'
| 'arrayReplace'
// biome-ignore lint/suspicious/noExplicitAny: -
| ((target: any, source: any) => any);
// #endregion MergeStrategy
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;
/**
* Merges multiple objects based on a specified merge strategy.
*
* @example
* ```ts
* const obj1 = { a: 1, b: { x: 10, y: "hello" }, c: [1] };
* const obj2 = { b: { y: 20, z: true }, c: [2] };
* const obj3 = { d: false, c: [3] };
*
* merge("deep", obj1, obj2, obj3); // { a: 1, b: { x: 10, y: 20, z: true }, c: [1, 2, 3], d: false }
* merge("shallow", obj1, obj2, obj3); // { a: 1, b: { y: 20, z: true }, c: [3], d: false }
* ```
*
* @param [strategy='deep'] - The merging strategy to use.
* @param items - The objects to merge.
* @returns A new merged object.
*/
export function merge<T extends Obj[]>(strategy: MergeStrategy = 'deep', ...items: [...T]): Merge<T> {
if (items.length === 0) return {} as Merge<T>;
if (strategy === 'shallow') {
return Object.assign({}, ...items) as Merge<T>;
}
return items.reduce((acc, obj) => deepMerge(acc, obj, strategy) as unknown as Merge<T>, {} as Merge<T>);
}
/**
* Deeply merges two objects based on the provided strategy.
*
* - Uses **direct property access** for performance.
* - **Avoids redundant deep merging** where unnecessary.
* - Optimized **array merging strategies**.
*
* @param target - The target object.
* @param source - The source object.
* @param strategy - The merge strategy.
* @returns A new merged object.
*/
function deepMerge<T extends Obj, U extends Obj>(target: T, source: U, strategy: MergeStrategy): DeepMerge<T, U> {
if (!isObject(source)) return source as DeepMerge<T, U>;
const result = { ...target } as DeepMerge<T, U>;
for (const key in source) {
if (!Object.hasOwn(source, key)) continue; // Prevent prototype pollution
const sourceValue = source[key];
const targetValue = result[key];
// biome-ignore lint/suspicious/noExplicitAny: -
(result as any)[key] =
isArray(sourceValue) && isArray(targetValue)
? handleArrayMerge(targetValue, sourceValue, strategy)
: isObject(sourceValue) && isObject(targetValue)
? deepMerge(targetValue, sourceValue, strategy)
: applyMergeStrategy(targetValue, sourceValue, strategy);
}
return result;
}
/**
* Optimized array merge based on strategy.
*
* - `"arrayConcat"` → Concatenates arrays.
* - `"arrayReplace"` → Replaces the existing array.
* - Default: **Unique merge** (Set-based optimization).
*/
function handleArrayMerge<T, U>(targetArray: T[] | undefined, sourceArray: U[], strategy: MergeStrategy): (T | U)[] {
if (!targetArray) return sourceArray;
// biome-ignore lint/suspicious/noExplicitAny: -
if (strategy === 'arrayConcat') return targetArray.concat(sourceArray as any);
if (strategy === 'arrayReplace') return sourceArray;
return Array.from(new Set([...targetArray, ...sourceArray])); // Unique merge
}
/**
* Determines the appropriate value to assign based on the merge strategy.
*
* - `"lastWins"` → Overwrites with the latest value.
* - Custom functions → Allows user-defined behavior.
*/
function applyMergeStrategy<T, U>(target: T, source: U, strategy: MergeStrategy): T | U {
if (typeof strategy === 'function') return strategy(target, source);
return strategy === 'lastWins' || source !== undefined ? source : target;
}Features
- Isomorphic: Works in both Browser and Node.js.
- Immutable: Never mutates the source objects; always returns a new object.
- Multiple Strategies: Built-in support for deep, shallow, array-specific, and custom merging.
- Type-safe: Properly merges types and handles multiple input objects.
API
Type Definitions
ts
type MergeStrategy =
| 'deep'
| 'shallow'
| 'lastWins'
| 'arrayConcat'
| 'arrayReplace'
// biome-ignore lint/suspicious/noExplicitAny: -
| ((target: any, source: any) => any);ts
function merge<T extends object[]>(strategy: MergeStrategy, ...items: T): any;Parameters
strategy: The merging algorithm to use:'deep': Recursively merges nested objects and arrays (default-like behavior).'shallow': Performs a shallow merge (similar toObject.assign).'lastWins': Only the last object's value for a given key is kept.'arrayConcat': Deep merge, but arrays are concatenated.'arrayReplace': Deep merge, but arrays are replaced by the later value.custom function: A function(target, source) => mergedValuefor fine-grained control.
...items: Two or more objects to merge.
Returns
- A new object containing the merged results.
Examples
Deep vs. Shallow Merge
ts
import { merge } from '@vielzeug/toolkit';
const obj1 = { a: 1, b: { x: 10 } };
const obj2 = { b: { y: 20 } };
merge('deep', obj1, obj2); // { a: 1, b: { x: 10, y: 20 } }
merge('shallow', obj1, obj2); // { a: 1, b: { y: 20 } }Array Strategies
ts
import { merge } from '@vielzeug/toolkit';
const defaults = { tags: ['new'] };
const overrides = { tags: ['featured'] };
merge('arrayConcat', defaults, overrides); // { tags: ['new', 'featured'] }
merge('arrayReplace', defaults, overrides); // { tags: ['featured'] }Custom Merge Strategy
ts
import { merge } from '@vielzeug/toolkit';
const custom = (target, source) => {
if (typeof target === 'number' && typeof source === 'number') {
return target + source; // Sum numbers instead of replacing
}
return source;
};
merge(custom, { val: 10 }, { val: 5 }); // { val: 15 }Implementation Notes
- Throws
TypeErrorif fewer than two objects are provided. - Circular references in source objects may cause a stack overflow during deep merge.
- The
deepstrategy treatsDate,RegExp, and other built-in objects as primitives (cloning them but not merging their internals).