Advanced customization options for controlling merge behavior across different data types, implementing custom merge strategies, and handling complex merging scenarios.
Comprehensive options for customizing both immutable and mutable merge operations.
/**
* Options for customizing immutable deepmerge operations
*/
interface DeepMergeOptions<
in out M,
MM extends Readonly<Record<PropertyKey, unknown>> = {}
> {
/** Custom function for merging objects/records, or false to disable */
mergeRecords?: DeepMergeMergeFunctions<M, MM>["mergeRecords"] | false;
/** Custom function for merging arrays, or false to treat as other types */
mergeArrays?: DeepMergeMergeFunctions<M, MM>["mergeArrays"] | false;
/** Custom function for merging Maps, or false to treat as other types */
mergeMaps?: DeepMergeMergeFunctions<M, MM>["mergeMaps"] | false;
/** Custom function for merging Sets, or false to treat as other types */
mergeSets?: DeepMergeMergeFunctions<M, MM>["mergeSets"] | false;
/** Custom function for merging other types (required if specified) */
mergeOthers?: DeepMergeMergeFunctions<M, MM>["mergeOthers"];
/** Function to update metadata during merging */
metaDataUpdater?: (previousMeta: M | undefined, metaMeta: Readonly<Partial<MM>>) => M;
/** Enable falling back to default merge when custom functions return undefined */
enableImplicitDefaultMerging?: boolean;
}
/**
* Options for customizing mutable deepmergeInto operations
*/
interface DeepMergeIntoOptions<
in out M,
MM extends Readonly<Record<PropertyKey, unknown>> = {}
> {
/** Custom function for merging objects/records into target, or false to disable */
mergeRecords?: DeepMergeMergeIntoFunctions<M, MM>["mergeRecords"] | false;
/** Custom function for merging arrays into target, or false to treat as other types */
mergeArrays?: DeepMergeMergeIntoFunctions<M, MM>["mergeArrays"] | false;
/** Custom function for merging Maps into target, or false to treat as other types */
mergeMaps?: DeepMergeMergeIntoFunctions<M, MM>["mergeMaps"] | false;
/** Custom function for merging Sets into target, or false to treat as other types */
mergeSets?: DeepMergeMergeIntoFunctions<M, MM>["mergeSets"] | false;
/** Custom function for merging other types into target (required if specified) */
mergeOthers?: DeepMergeMergeIntoFunctions<M, MM>["mergeOthers"];
/** Function to update metadata during merging */
metaDataUpdater?: (previousMeta: M | undefined, metaMeta: Readonly<Partial<MM>>) => M;
/** Enable falling back to default merge when custom functions return undefined */
enableImplicitDefaultMerging?: boolean;
}Special symbols that control merge behavior within custom merge functions.
/**
* Special values that tell deepmerge to perform certain actions
*/
const actions: {
/** Indicates that the default merge strategy should be used */
readonly defaultMerge: symbol;
/** Indicates that merging should be skipped for this value */
readonly skip: symbol;
};
/**
* Special values that tell deepmergeInto to perform certain actions
*/
const actionsInto: {
/** Indicates that the default merge strategy should be used */
readonly defaultMerge: symbol;
};Usage Examples:
import { deepmergeCustom } from "deepmerge-ts";
// Skip merging certain properties
const mergeWithSkip = deepmergeCustom({
mergeRecords: (records, utils, meta) => {
const result = {};
const allKeys = utils.getKeys(records);
for (const key of allKeys) {
if (key === 'skipThis' || key.startsWith('_')) {
return utils.actions.skip; // Skip this entire merge operation
}
const values = records
.filter(record => utils.objectHasProperty(record, key))
.map(record => record[key]);
if (values.length > 0) {
result[key] = utils.deepmerge(values);
}
}
return result;
}
});
// Use default merge for specific conditions
const conditionalMerge = deepmergeCustom({
mergeArrays: (arrays, utils, meta) => {
// Use default merge for arrays with fewer than 10 items
const totalItems = arrays.reduce((sum, arr) => sum + arr.length, 0);
if (totalItems < 10) {
return utils.actions.defaultMerge;
}
// Custom merge for large arrays - take only first 5 from each
return arrays.flatMap(arr => arr.slice(0, 5));
}
});Detailed interfaces for implementing custom merge strategies.
/**
* Interface for immutable merge functions
*/
interface DeepMergeMergeFunctions<M, MM extends DeepMergeBuiltInMetaData> {
/** Merge plain objects/records */
mergeRecords: <
Ts extends ReadonlyArray<Record<PropertyKey, unknown>>,
U extends DeepMergeMergeFunctionUtils<M, MM>,
MF extends DeepMergeMergeFunctionsURIs
>(
values: Ts,
utils: U,
meta: M | undefined
) => DeepMergeRecordsDefaultHKT<Ts, MF, M> | symbol;
/** Merge arrays */
mergeArrays: <
Ts extends ReadonlyArray<ReadonlyArray<unknown>>,
U extends DeepMergeMergeFunctionUtils<M, MM>
>(
values: Ts,
utils: U,
meta: M | undefined
) => DeepMergeArraysDefaultHKT<Ts, never, M> | symbol;
/** Merge Sets */
mergeSets: <
Ts extends ReadonlyArray<ReadonlySet<unknown>>,
U extends DeepMergeMergeFunctionUtils<M, MM>
>(
values: Ts,
utils: U,
meta: M | undefined
) => DeepMergeSetsDefaultHKT<Ts> | symbol;
/** Merge Maps */
mergeMaps: <
Ts extends ReadonlyArray<ReadonlyMap<unknown, unknown>>,
U extends DeepMergeMergeFunctionUtils<M, MM>
>(
values: Ts,
utils: U,
meta: M | undefined
) => DeepMergeMapsDefaultHKT<Ts> | symbol;
/** Merge other types */
mergeOthers: <
Ts extends ReadonlyArray<unknown>,
U extends DeepMergeMergeFunctionUtils<M, MM>
>(
values: Ts,
utils: U,
meta: M | undefined
) => DeepMergeLeaf<Ts> | symbol;
}
/**
* Interface for mutable merge functions
*/
interface DeepMergeMergeIntoFunctions<M, MM extends DeepMergeBuiltInMetaData> {
/** Merge plain objects/records into target */
mergeRecords: <
Ts extends ReadonlyArray<Record<PropertyKey, unknown>>,
U extends DeepMergeMergeIntoFunctionUtils<M, MM>
>(
target: Reference<Record<PropertyKey, unknown>>,
values: Ts,
utils: U,
meta: M | undefined
) => void | symbol;
/** Merge arrays into target */
mergeArrays: <
Ts extends ReadonlyArray<ReadonlyArray<unknown>>,
U extends DeepMergeMergeIntoFunctionUtils<M, MM>
>(
target: Reference<unknown[]>,
values: Ts,
utils: U,
meta: M | undefined
) => void | symbol;
/** Merge Sets into target */
mergeSets: <
Ts extends ReadonlyArray<ReadonlySet<unknown>>,
U extends DeepMergeMergeIntoFunctionUtils<M, MM>
>(
target: Reference<Set<unknown>>,
values: Ts,
utils: U,
meta: M | undefined
) => void | symbol;
/** Merge Maps into target */
mergeMaps: <
Ts extends ReadonlyArray<ReadonlyMap<unknown, unknown>>,
U extends DeepMergeMergeIntoFunctionUtils<M, MM>
>(
target: Reference<Map<unknown, unknown>>,
values: Ts,
utils: U,
meta: M | undefined
) => void | symbol;
/** Merge other types into target */
mergeOthers: <
Ts extends ReadonlyArray<unknown>,
U extends DeepMergeMergeIntoFunctionUtils<M, MM>
>(
target: Reference<unknown>,
values: Ts,
utils: U,
meta: M | undefined
) => void | symbol;
}Comprehensive utility interfaces provided to custom merge functions.
/**
* Utility interface provided to immutable merge functions
*/
interface DeepMergeMergeFunctionUtils<M, MM extends DeepMergeBuiltInMetaData> {
/** Default merge function implementations */
defaultMergeFunctions: DeepMergeMergeFunctionsDefaults;
/** Current merge function configuration */
mergeFunctions: DeepMergeMergeFunctions<M, MM>;
/** Metadata updater function */
metaDataUpdater: (previousMeta: M | undefined, metaMeta: Readonly<Partial<MM>>) => M;
/** Recursive deepmerge function for nested merging */
deepmerge: (...objects: readonly unknown[]) => unknown;
/** Whether implicit default merging is enabled */
useImplicitDefaultMerging: boolean;
/** Special action symbols */
actions: typeof actions;
/** Utility functions */
getKeys: typeof getKeys;
getObjectType: typeof getObjectType;
objectHasProperty: typeof objectHasProperty;
}
/**
* Utility interface provided to mutable merge functions
*/
interface DeepMergeMergeIntoFunctionUtils<M, MM extends DeepMergeBuiltInMetaData> {
/** Default merge into function implementations */
defaultMergeFunctions: DeepMergeMergeIntoFunctionsDefaults;
/** Current merge into function configuration */
mergeFunctions: DeepMergeMergeIntoFunctions<M, MM>;
/** Metadata updater function */
metaDataUpdater: (previousMeta: M | undefined, metaMeta: Readonly<Partial<MM>>) => M;
/** Recursive deepmergeInto function for nested merging */
deepmergeInto: (target: object, ...objects: readonly unknown[]) => void;
/** Utility for merging unknown values into target */
mergeUnknownsInto: (
target: Reference<unknown>,
values: ReadonlyArray<unknown>,
utils: DeepMergeMergeIntoFunctionUtils<M, MM>,
meta: M | undefined
) => void;
/** Whether implicit default merging is enabled */
useImplicitDefaultMerging: boolean;
/** Special action symbols */
actions: typeof actionsInto;
/** Utility functions */
getKeys: typeof getKeys;
getObjectType: typeof getObjectType;
objectHasProperty: typeof objectHasProperty;
}Comprehensive examples demonstrating complex customization scenarios.
Custom Array Merging Strategies:
import { deepmergeCustom, deepmergeIntoCustom } from "deepmerge-ts";
// Strategy 1: Replace arrays instead of concatenating
const replaceArrays = deepmergeCustom({
mergeArrays: (arrays) => arrays[arrays.length - 1]
});
// Strategy 2: Merge arrays as sets (remove duplicates)
const mergeArraysAsSet = deepmergeCustom({
mergeArrays: (arrays) => {
const combined = arrays.flat();
return Array.from(new Set(combined));
}
});
// Strategy 3: Interleave arrays
const interleaveArrays = deepmergeCustom({
mergeArrays: (arrays) => {
if (arrays.length === 0) return [];
const maxLength = Math.max(...arrays.map(arr => arr.length));
const result = [];
for (let i = 0; i < maxLength; i++) {
for (const array of arrays) {
if (i < array.length) {
result.push(array[i]);
}
}
}
return result;
}
});
// Strategy 4: Conditional array merging based on content
const smartArrayMerge = deepmergeCustom({
mergeArrays: (arrays, utils, meta) => {
// If all arrays contain numbers, sum duplicates
const allNumbers = arrays.every(arr =>
arr.every(item => typeof item === 'number')
);
if (allNumbers) {
const numberMap = new Map<number, number>();
for (const array of arrays) {
for (const num of array as number[]) {
numberMap.set(num, (numberMap.get(num) || 0) + 1);
}
}
return Array.from(numberMap.entries()).map(([num, count]) =>
count > 1 ? num * count : num
);
}
// Default concatenation for other types
return arrays.flat();
}
});Custom Object Merging with Validation:
import { deepmergeCustom, actions } from "deepmerge-ts";
// Validate and merge objects with schema enforcement
const schemaValidatedMerge = deepmergeCustom({
mergeRecords: (records, utils, meta) => {
const result = {};
const allKeys = utils.getKeys(records);
for (const key of allKeys) {
const values = records
.filter(record => utils.objectHasProperty(record, key))
.map(record => record[key]);
// Validate key naming convention
if (typeof key === 'string' && key.startsWith('__')) {
console.warn(`Skipping private property: ${key}`);
continue;
}
// Type validation
const types = values.map(v => typeof v);
const uniqueTypes = new Set(types);
if (uniqueTypes.size > 1) {
console.warn(`Type mismatch for key ${String(key)}: ${Array.from(uniqueTypes).join(', ')}`);
// Use the last value's type as authoritative
result[key] = values[values.length - 1];
} else if (values.length === 1) {
result[key] = values[0];
} else {
// Recursively merge if all values are objects
const firstType = utils.getObjectType(values[0]);
if (firstType === utils.ObjectType.RECORD) {
result[key] = utils.deepmerge(values);
} else {
result[key] = values[values.length - 1];
}
}
}
return result;
}
});Metadata-Driven Merging:
import { deepmergeCustom } from "deepmerge-ts";
interface MergeMetadata {
path: string[];
depth: number;
strategy?: 'replace' | 'merge' | 'concat';
}
const metadataDrivenMerge = deepmergeCustom<unknown, {}, MergeMetadata>({
metaDataUpdater: (previousMeta, metaMeta) => ({
path: previousMeta?.path || [],
depth: (previousMeta?.depth || 0) + 1,
strategy: metaMeta.strategy || previousMeta?.strategy || 'merge'
}),
mergeRecords: (records, utils, meta) => {
const strategy = meta?.strategy || 'merge';
if (strategy === 'replace') {
return records[records.length - 1];
}
const result = {};
const allKeys = utils.getKeys(records);
for (const key of allKeys) {
const values = records
.filter(record => utils.objectHasProperty(record, key))
.map(record => record[key]);
if (values.length > 0) {
const childMeta = utils.metaDataUpdater(meta, {
path: [...(meta?.path || []), String(key)],
key,
values
});
result[key] = utils.deepmerge(values, childMeta);
}
}
return result;
},
mergeArrays: (arrays, utils, meta) => {
const strategy = meta?.strategy || 'concat';
switch (strategy) {
case 'replace':
return arrays[arrays.length - 1];
case 'merge':
// Merge arrays as if they were objects with numeric keys
const merged = {};
arrays.forEach((array, arrayIndex) => {
array.forEach((item, itemIndex) => {
const key = `${arrayIndex}-${itemIndex}`;
merged[key] = item;
});
});
return Object.values(merged);
default:
return arrays.flat();
}
}
}, { path: [], depth: 0 });
// Usage with metadata
const result = metadataDrivenMerge(
{
config: { timeout: 1000 },
items: [1, 2, 3]
},
{
config: { retries: 3 },
items: [4, 5, 6]
}
);Performance-Optimized Custom Merging:
import { deepmergeIntoCustom } from "deepmerge-ts";
// High-performance mutable merging for large datasets
const performanceMerge = deepmergeIntoCustom({
mergeRecords: (target, records, utils, meta) => {
// Batch updates to reduce overhead
const updates = new Map();
// Collect all updates first
for (const record of records) {
for (const [key, value] of Object.entries(record)) {
updates.set(key, value);
}
}
// Apply updates in batch
for (const [key, value] of updates) {
if (typeof value === 'object' && value !== null &&
target.value[key] && typeof target.value[key] === 'object') {
// Recursive merge for nested objects
utils.deepmergeInto(target.value[key], value);
} else {
target.value[key] = value;
}
}
},
mergeArrays: (target, arrays, utils, meta) => {
// Pre-allocate space for better performance
const totalLength = arrays.reduce((sum, arr) => sum + arr.length, 0);
const currentLength = target.value.length;
// Extend array to final size
target.value.length = currentLength + totalLength;
// Copy elements in chunks for better memory access patterns
let insertIndex = currentLength;
for (const array of arrays) {
for (let i = 0; i < array.length; i++) {
target.value[insertIndex++] = array[i];
}
}
},
mergeMaps: (target, maps, utils, meta) => {
// Batch Map updates
for (const map of maps) {
for (const [key, value] of map) {
target.value.set(key, value);
}
}
},
mergeSets: (target, sets, utils, meta) => {
// Batch Set updates
for (const set of sets) {
for (const value of set) {
target.value.add(value);
}
}
}
});Advanced metadata handling for context-aware merging.
/**
* Built-in metadata type for merge operations
*/
type DeepMergeBuiltInMetaData = Readonly<Record<PropertyKey, unknown>>;
/**
* Metadata updater function signature
*/
type MetaDataUpdater<
in out M,
MM extends DeepMergeBuiltInMetaData = DeepMergeBuiltInMetaData,
> = (previousMeta: M | undefined, metaMeta: Readonly<Partial<MM>>) => M;
/**
* Default metadata updater implementation
*/
const defaultMetaDataUpdater: MetaDataUpdater<DeepMergeBuiltInMetaData>;Custom Metadata Usage:
interface CustomMetadata {
operationId: string;
timestamp: number;
mergeCount: number;
validationRules?: string[];
}
const metadataAwareMerge = deepmergeCustom<unknown, {}, CustomMetadata>({
metaDataUpdater: (previousMeta, metaMeta) => ({
operationId: metaMeta.operationId || previousMeta?.operationId || crypto.randomUUID(),
timestamp: Date.now(),
mergeCount: (previousMeta?.mergeCount || 0) + 1,
validationRules: metaMeta.validationRules || previousMeta?.validationRules
}),
mergeRecords: (records, utils, meta) => {
console.log(`Merge operation ${meta?.operationId} at depth ${meta?.mergeCount}`);
if (meta?.validationRules?.includes('no-private-keys')) {
// Filter out private keys during merge
const filteredRecords = records.map(record => {
const filtered = {};
for (const [key, value] of Object.entries(record)) {
if (!key.startsWith('_')) {
filtered[key] = value;
}
}
return filtered;
});
return utils.defaultMergeFunctions.mergeRecords(filteredRecords, utils, meta);
}
return utils.defaultMergeFunctions.mergeRecords(records, utils, meta);
}
}, {
operationId: 'initial',
timestamp: Date.now(),
mergeCount: 0,
validationRules: ['no-private-keys']
});