merge-change
v3.0.17
Published
Advanced library for deep merging, patching, and immutable updates of data structures. Features declarative operations for specific merging behaviors, property management, custom type merging rules, and difference tracking. Supports complex data transform
Maintainers
Readme
merge-change
merge-change is a flexible JavaScript and TypeScript library for deep merge operations, state updates, and structural data transformations. It supports custom merge rules, declarative operations, and immutable or in-place updates.
The library provides a simple but flexible API for complex data transformations with support for declarative operations, custom type merging rules, and difference tracking. You can define your own merging algorithm between specific data types. The main difference from other deep merging libraries is the ability to not only merge object properties but also delete and redefine values within a single merge operation.
Table of Contents
- Installation
- Core Functions
- Declarative Operations
- Custom Merge Logic
- Utility Functions
- TypeScript Support
- Path Format Options
- License
Installation
Install with npm:
npm install --save merge-changeFunctions
merge(...values: T): MergedAll<T>
Deep merge all values without changing them. Creates a new instance. This is ideal for creating a new object by extending the original object.
import {merge} from 'merge-change';
const first = {
property: {
one: true,
two: 2,
},
other: {
value: 'string'
}
};
const second = {
property: {
three: 3,
two: 222,
inner: [1, 2, 3]
}
};
// Merge first with second and create new object
const result = merge(first, second)
expect(result).toEqual({
property: {
one: true,
two: 222, // replaced number
three: 3, // new
inner: [1, 2, 3] // by default array replaced
},
other: {
value: 'string'
}
});
// First object not changed
expect(first).toEqual({
property: {
one: true,
two: 2,
inner: [0, 1]
},
other: {
value: 'string'
}
});
// Second object not changed
expect(second).toEqual({
property: {
three: 3,
two: 222,
inner: [1, 2, 3]
}
});update(...values: T): MergedAll<T>
Performs an immutable merge, creating new instances only for properties that have changed. This is perfect for state management in frameworks like React or Redux, as it preserves object references for unchanged parts of the data structure.
import {update} from 'merge-change';
const source = {
property: {
one: true,
two: 2,
deepProp: {
field: 1
}
},
other: {
value: 'string'
}
};
const updates = {
property: {
three: 3,
two: 222,
inner: [1, 2, 3]
}
};
// Immutable update of first value with second. Return new instance if changed.
const newSource = update(source, updates)
expect(newSource).toEqual({
property: {
one: true,
two: 222, // replaced number
deepProp: { // reference to first.property.deepProp
field: 1
},
three: 3, // new
inner: [1, 2, 3] // by default array replaced
},
other: {
value: 'string'
}
});
// First object not changed!
expect(first).toEqual({
property: {
one: true,
two: 2,
inner: [0, 1]
},
other: {
value: 'string'
}
});
// Second object not changed!
expect(updates).toEqual({
property: {
three: 3,
two: 222,
inner: [1, 2, 3]
}
});
// But the property "newSource.other" is a reference to "first.other" because it is not changed
expect(first.other).toBe(newSource.other)
// Also the property "newSource.property.deepProp" is a reference to "first.property.deepProp" because it is not changed
expect(first.property.deepProp).toBe(newSource.property.deepProp)patch(...values: T): MergedAll<T>
Merges objects by mutating the original object. This is useful for patching existing objects without creating new instances.
import {patch} from 'merge-change';
const original = {
a: {
one: true,
two: 2,
deepProp: { // reference to first.property.deepProp
field: 1
},
}
};
const patchData = {
a: {
three: 3,
}
};
const newOriginal = patch(original, patchData)
expect(newOriginal).toEqual({
a: {
one: true,
two: 2,
deepProp: {
field: 1
},
three: 3,
}
});
// First original object is a newOriginal
expect(newOriginal).toBe(original);
// Second object not changed!
expect(patchData).toEqual({
a: {
three: 3,
}
});Declarative Operations
Declarative operations in the second or subsequent arguments allow you to perform deletion,
reassignment, and other actions within a single merge operation. Declarative operations are
supported in all methods: merge, update, patch.
$set
Sets or replaces properties without deep merging. The keys is a deep path to the property. Declarative operations can be combined with regular values.
const result = merge(
{
a: {
one: 1,
two: 2
},
b: {
x: 200,
y: 100
},
deepProp: {
field: 1
},
},
{
$set: { // declarative operation
a: { // for replace value for "a"
three: 3
},
'deepProp.field': 20 // Property keys can be paths
},
b: { // values for normal merge
y: 300
}
}
);
expect(result).toEqual({
a: { // replaced
three: 3
},
b: {
x: 200,
y: 300 // changed
},
deepProp: {
field: 1
},
});$unset
Removes properties by name or path.
const result = merge(
{
a: {
one: 1,
two: 2
}
},
{
$unset: ['a.two']
}
);
expect(result).toEqual({
a: {
one: 1
}
});You can use the asterisk (*) to remove all properties:
const result = merge(
{
a: {
one: 1,
two: 2
}
},
{
$unset: ['a.*']
}
);
expect(result).toEqual({
a: {}
})
$leave
Keeps only specified properties, removing all others.
import {merge} from 'merge-change';
const result = merge(
{
a: {
one: 1,
two: 2,
three: 3
}
},
{
a: {
$leave: ['two']
}
}
);
expect(result).toEqual({
a: {
two: 2
}
})$push
Adds values to array properties.
const result = merge(
{
prop1: ['a', 'b'],
prop2: ['a', 'b']
},
{
$push: {
prop1: ['c', 'd'],
prop2: {x: 'c'}
}
}
);
expect(result).toEqual({
prop1: ['a', 'b', ['c', 'd']],
prop2: ['a', 'b', {x: 'c'}]
})$concat
Concatenates arrays.
const result = merge(
{
prop1: ['a', 'b'],
prop2: ['a', 'b']
},
{
$concat: {
prop1: ['c', 'd'],
prop2: {x: 'c'}
}
}
);
expect(result).toEqual({
prop1: ['a', 'b', 'c', 'd'],
prop2: ['a', 'b', {x: 'c'}]
})$pull
Removes elements from arrays by value equality.
const result = merge(
{
items: [1, 2, 3, 2, 4]
},
{
$pull: {
items: 2
}
}
);
expect(result).toEqual({
items: [1, 3, 4]
})Custom Merge Logic
You can customize how specific types are merged by creating custom merge functions with the factory
functions createMerge, createUpdate, and createPatch.
Custom merge methods are named using the pattern TypeName1_TypeName2, where:
TypeName1is the native type or constructor name of the first valueTypeName2is the native type or constructor name of the second value
The type names are determined by the type() function, which returns:
- Native TypeScript types:
'string','number','boolean','object','Array', etc. - Class names:
'Date','Map','Set','MyCustomClass', etc. - Special types:
'null','undefined' unknowntype for merging with any types
import {createMerge, createUpdate, createPatch} from 'merge-change';
// Create custom merge methods
const customMethods = {
// Method name is formed from the types: Array_Array - that always concatenate two arrays
Array_Array(first, second, kind, mc) {
// merge - create new array with deep clone
if (kind === 'merge') {
return first.concat(second).map(item => mc(undefined, item));
}
// patch - mutate first array
if (kind === 'patch') {
first.splice(first.length, 0, ...second);
return first;
}
// update - return first array if second is empty, or create new without clone
if (second.length === 0) {
return first;
} else {
return first.concat(second);
}
},
// Example with custom class
MyClass_object(first, second, kind, mc) {
// Custom logic for merging MyClass with a plain object
// ...
},
// Example with custom class and others types
MyClass_unknown(first, second, kind, mc) {
// Custom logic for merging MyClass with any other types
// ...
},
// Example with native types
number_string(first, second, kind, mc) {
// Custom logic for merging a number with a string
// ...
}
};
// Create custom merge functions
const customMerge = createMerge(customMethods);
const customUpdate = createUpdate(customMethods);
const customPatch = createPatch(customMethods);
// Test the custom merge function
const result = customMerge(
{items: [1, 2]},
{items: [3, 4]}
);
expect(result).toBeDefined(); // { items: [1, 2, 3, 4] }Utility Functions
get(data, path, defaultValue = undefined, separator = '.')
Retrieves a value from a nested object using a string path.
import {get} from 'merge-change';
const obj = {
a: {
b: {
c: 'value'
},
items: [1, 2, 3]
}
};
// Get a nested property
const value1 = get(obj, 'a.b.c');
expect(value1).toBeDefined(); // 'value'
// Get an array element
const value2 = get(obj, 'a.items.1');
expect(value2).toBeDefined(); // 2
// Get with a default value for non-existent paths
const value3 = get(obj, 'a.x.y', 'default');
expect(value3).toBeDefined(); // 'default'
// Get with a custom separator
const value4 = get(obj, 'a/b/c', undefined, '/');
expect(value4).toBeDefined(); // 'value'set(data, path, value, skipExisting = false, separator = '.')
Sets a value in a nested object using a path, creating intermediate objects if needed.
import {set} from 'merge-change';
const obj = {
a: {
b: {}
}
};
// Set a nested property
set(obj, 'a.b.c', 'value');
expect(obj).toBeDefined(); // { a: { b: { c: 'value' } } }
// Set with a custom separator
set(obj, 'a/b/d', 'another value', false, '/');
expect(obj).toBeDefined(); // { a: { b: { c: 'value', d: 'another value' } } }
// Set only if the property doesn't exist
set(obj, 'a.b.c', 'new value', true);
expect(obj).toBeDefined(); // { a: { b: { c: 'value', d: 'another value' } } }
// Create arrays when using numeric indices
set(obj, 'a.items.0', 'first');
set(obj, 'a.items.1', 'second');
expect(obj).toBeDefined(); // { a: { b: { c: 'value', d: 'another value' }, items: ['first', 'second'] } }unset(data, path, separator = '.')
Removes a property from a nested object using a path.
import {unset} from 'merge-change';
const obj = {
a: {
b: {
c: 'value',
d: 'another value'
},
items: [1, 2, 3]
}
};
// Remove a nested property
unset(obj, 'a.b.c');
expect(obj).toBeDefined(); // { a: { b: { d: 'another value' }, items: [1, 2, 3] } }
// Remove an array element
unset(obj, 'a.items.1');
expect(obj).toBeDefined(); // { a: { b: { d: 'another value' }, items: [1, 3] } }
// Remove all properties using asterisk
unset(obj, 'a.b.*');
expect(obj).toBeDefined(); // { a: { b: {}, items: [1, 3] } }
// Remove with a custom separator
unset(obj, 'a/items', '/');
expect(obj).toBeDefined(); // { a: { b: {} } }diff(source, compare, options)
Calculates the difference between two objects, returning an object with $set and $unset operations.
import {diff} from 'merge-change';
const first = {
name: 'value',
profile: {
surname: 'Surname',
birthday: new Date('2000-01-01'),
avatar: {
url: 'pic.png'
}
},
access: [100, 350, 200],
secret: 'x'
};
const second = {
login: 'value',
profile: {
surname: 'Surname2',
avatar: {
url: 'new/pic.png'
}
},
access: [700]
};
// Calculate differences, ignoring the 'secret' property
const result = diff(first, second, {
ignore: ['secret'],
separator: '/'
});
expect(result).toBeDefined();
// {
// $set: {
// 'login': 'value',
// 'profile/surname': 'Surname2',
// 'profile/avatar/url': 'new/pic.png',
// 'access': [700]
// },
// $unset: [
// 'profile/birthday',
// 'name'
// ]
// }
// Apply the differences to the original object
import {merge} from 'merge-change';
const updated = merge(first, result);
expect(updated).toBeDefined();
// Similar to 'second' but with 'secret' preservedtype(value)
Returns the constructor name of a value.
import {type} from 'merge-change';
expect(type(null).toBeDefined();
)
; // 'null'
expect(type(true).toBeDefined();
)
; // 'boolean'
expect(type({}).toBeDefined();
)
; // 'object'
expect(type([]).toBeDefined();
)
; // 'Array'
expect(type(new Date().toBeDefined();
))
; // 'Date'
expect(type(new Map().toBeDefined();
))
; // 'Map'
expect(type(new Set().toBeDefined();
))
; // 'Set'isInstanceof(value, typeName)
Checks if a value belongs to a class by the string name of the class.
import {isInstanceof} from 'merge-change';
expect(isInstanceof(100, 'Number').toBeDefined();
)
; // true
expect(isInstanceof(new Date().toBeDefined();
,
'Date'
))
; // true
expect(isInstanceof(new Date().toBeDefined();
,
'Object'
))
; // true
expect(isInstanceof({}, 'Array').toBeDefined();
)
; // false
// Works with custom classes too
class MyClass {
}
expect(isInstanceof(new MyClass().toBeDefined();
,
'MyClass'
))
; // trueplain(value, recursive = true)
Converts a deep value to plain types if the value has a plain representation.
import {plain} from 'merge-change';
const obj = {
date: new Date('2021-01-07T19:10:21.759Z'),
prop: {
id: '6010a8c75b9b393070e42e68'
},
regex: /test/,
fn: function () {
}
};
const result = plain(obj);
expect(result).toBeDefined();
// {
// date: '2021-01-07T19:10:21.759Z',
// prop: {
// id: '6010a8c75b9b393070e42e68'
// },
// regex: /test/,
// fn: [Function]
// }flat(value, separator = '.', clearUndefined = false)
Converts a nested structure to a flat object with path-based keys.
import {flat} from 'merge-change';
const obj = {
a: {
b: {
c: 100
}
},
d: [1, 2, {
e: 'value'
}]
};
// Flatten the object
const result = flat(obj, 'root', '.');
expect(result).toBeDefined();
// {
// 'root.a.b.c': 100,
// 'root.d.0': 1,
// 'root.d.1': 2,
// 'root.d.2.e': 'value'
// }
// Flatten with a different separator
const result2 = flat(obj, '', '/');
expect(result2).toBeDefined();
// {
// 'a/b/c': 100,
// 'd/0': 1,
// 'd/1': 2,
// 'd/2/e': 'value'
// }match(value, pattern)
Compares a value to a pattern structure and checks if the value conforms to the same shape. Useful for validating if a structure contains expected properties or matches a certain schema.
import {match} from 'merge-change';
const data = {
user: {
name: 'Alice',
age: 30
},
loggedIn: true
};
const pattern = {
user: {
name: 'string',
age: 'number'
},
loggedIn: 'boolean'
};
// Check if `data` matches the pattern shape
expect(match(data, pattern)).toBe(true);TypeScript Support
The library provides comprehensive TypeScript support with type-safe path operations.
Path Types
The library includes several utility types for working with paths:
Patch<Obj>: Enables partial updates for typeObj: objects combinePatchOperation<Obj>($set, $unset, $pull, $push, $concat) with recursive partial patching of fields; arrays patch elements recursively asPatch<U>[]; primitives remain asObj.ExtractPaths<Obj, Sep>: Extracts all possible paths in an object, including array indices.ExtractPathsStarted<Obj, Sep>: Extracts paths that start with a separator.ExtractPathsAny<Obj, Sep>: Union ofExtractPathsandExtractPathsStarted.ExtractPathsLeaf<Obj, Sep>: Extracts paths only to leaf properties of an object.ExtractPathsAsterisk<Obj, Sep>: Extracts paths with asterisks for operations that clear all properties or elements.PathToType<T, P, Sep>: Extracts the value type for a specific path.
import {ExtractPaths, PathToType} from 'merge-change';
// Define a type
type User = {
id: string;
profile: {
name: string;
age: number;
};
posts: Array<{
id: string;
title: string;
}>;
};
// Extract all possible paths
type UserPaths = ExtractPaths<User, '.'>;
// UserPaths = "id" | "profile" | "profile.name" | "profile.age" | "posts" | "posts.0" | "posts.0.id" | "posts.0.title" | ...
// Get the type of a specific path
type PostTitle = PathToType<User, 'posts.0.title', '.'>;
// PostTitle = stringType Safety
The library's functions are type-safe, providing autocompletion and type checking for paths:
import {get, set, unset} from 'merge-change';
const user = {
id: '123',
profile: {
name: 'John',
age: 30
},
posts: [
{id: 'p1', title: 'First Post'}
]
};
// Type-safe get
const name = get(user, 'profile.name'); // Type: string
const post = get(user, 'posts.0'); // Type: { id: string, title: string }
// Type-safe set
set(user, 'profile.age', 31); // OK
set(user, 'posts.0.title', 'Updated Post'); // OK
// @ts-expect-error - Type error: 'invalid' is not a valid path
set(user, 'invalid.path', 'value');
// Type-safe unset
unset(user, 'profile.name'); // OK
unset(user, 'posts.0'); // OK
// @ts-expect-error - Type error: 'invalid' is not a valid path
unset(user, 'invalid.path');Path Format Options
The library supports different path formats:
- Dot notation (default):
'a.b.c' - Slash notation:
'a/b/c' - Custom separator: Any string can be used as a separator
All functions that accept paths (get, set, unset, diff, etc.) allow specifying a custom
separator:
import {get, set, unset, diff} from 'merge-change';
const obj = {
a: {
b: {
c: 'value'
}
}
};
// Using dot notation (default)
get(obj, 'a.b.c'); // 'value'
// Using slash notation
get(obj, 'a/b/c', undefined, '/'); // 'value'
// Using custom separator
get(obj, 'a::b::c', undefined, '::'); // 'value'
// The same applies to set, unset, diff, etc.
set(obj, 'x/y/z', 'new value', false, '/');
unset(obj, 'a::b', '::');
diff(obj1, obj2, {separator: '/'});License
Author VladimirShestakov. Released under the MIT License.
