A library for deep operations on JavaScript objects — cloning, equality, merging, and flattening.
npm install @cleverbrush/deep
import { deepClone, deepEqual, deepExtend, deepFlatten } from '@cleverbrush/deep';
deepClone<T>(value: T): TCreates an isolated copy of plain data while preserving its TypeScript type.
import { deepClone } from '@cleverbrush/deep';
const input = { tags: ['a'], savedAt: new Date(1) };
const snapshot = deepClone(input);
input.tags.push('b');
input.savedAt.setTime(2);
// snapshot is still { tags: ['a'], savedAt: new Date(1) }
| Value | deepClone |
deepEqual |
|---|---|---|
| Primitives, functions | Returned unchanged | Object.is |
| Plain objects (including null prototypes) | Recursively cloned; prototype preserved | Enumerable own properties compared structurally; null and ordinary prototypes may compare equal |
| Arrays | Recursively cloned; length and holes preserved | Length, holes, elements and enumerable extra properties compared |
| Dates | Cloned by timestamp, including invalid dates | Timestamps compared with Object.is; two invalid dates compare equal |
| Files, Maps, Sets, typed arrays, custom instances and other objects | Returned by reference | Identity only |
Cloning preserves cycles and repeated references, including shared Dates. Own
enumerable string and symbol properties are copied as writable data properties.
Getters are read once per copied property; accessor descriptors, non-enumerable
properties and frozen/sealed state are not preserved. Date timestamps and array
length are copied explicitly. Special keys such as __proto__ are defined as own
properties without invoking inherited setters. This is a data snapshot utility,
not a clone of arbitrary object internals or a replacement for serialization.
deepEqual(a, b, options?)Recursively compares supported data values according to the table above. It handles nulls and cycles, and does not require identical reference-sharing topology: one shared child can compare equal to two separate, structurally equal children. Object key order does not matter; symbol keys participate by identity. Dates compare only their timestamps, not extra properties. Other opaque objects are never traversed.
Primitives follow Object.is: NaN equals NaN, but 0 differs from -0.
Array holes differ from explicit undefined. With disregardArrayOrder, each
element must match one unused element on the other side, preserving duplicate and
hole counts. Named/symbol array properties still compare by key. Inputs are not
sorted or mutated, and hash collisions cannot decide equality. Unordered matching
can require quadratic comparisons; prefer ordered equality for large arrays.
Parameters:
| Name | Type | Default | Description |
|---|---|---|---|
a |
any |
— | First value |
b |
any |
— | Second value |
options.disregardArrayOrder |
boolean |
false |
When true, arrays are treated as equal regardless of element order |
import { deepEqual } from '@cleverbrush/deep';
deepEqual({ a: { b: 1 } }, { a: { b: 1 } });
// => true
deepEqual({ a: { b: 1 } }, { a: { b: 20 } });
// => false
deepEqual({ a: { b: 1, c: 2 } }, { a: { b: 1 } });
// => false
// Array order can be ignored
deepEqual([1, 2, 3], [3, 1, 2], { disregardArrayOrder: true });
// => true
deepEqual([1, 1, 2], [1, 2, 2], { disregardArrayOrder: true });
// => false (duplicate counts differ)
deepEqual(new Map(), new Map());
// => false (opaque objects compare by identity)
To compare Maps/Sets/custom instances by content, explicitly project their relevant state into plain data first. For upgrade considerations, see the v4.x-to-v5 migration guide.
deepExtend(...objects)Deeply merges multiple objects. Works like Object.assign, but recursively merges nested objects instead of overwriting them. All arguments must be non-null objects.
Returns a new object that is the deep merge of all provided objects.
Keys that can mutate the prototype chain (__proto__, constructor, and prototype) are ignored.
import { deepExtend } from '@cleverbrush/deep';
const result = deepExtend(
{},
{
a: 'something',
name: {
first: 'Ivan'
}
},
{
name: {
last: 'Ivanov'
}
}
);
// result:
// {
// a: 'something',
// name: {
// first: 'Ivan',
// last: 'Ivanov'
// }
// }
The result type is inferred from the input types using the Merge<T> utility type, which is also exported from the library.
deepFlatten(obj, delimiter?)Flattens a nested object to a single level, concatenating keys with the specified delimiter.
Parameters:
| Name | Type | Default | Description |
|---|---|---|---|
obj |
Record<string, any> |
— | Object to flatten |
delimiter |
string |
'.' |
Separator used to join nested keys |
Throws an error if the object contains circular references.
import { deepFlatten } from '@cleverbrush/deep';
deepFlatten({
a: {
b: 1,
c: 2
},
d: 3
});
// => { 'a.b': 1, 'a.c': 2, d: 3 }
deepFlatten(
{
a: {
b: 1,
c: 2
},
d: {
e: {
f: 3
}
}
},
'-'
);
// => { 'a-b': 1, 'a-c': 2, 'd-e-f': 3 }
HashObject(obj, exclude?)Generates a hash representation for an object. Useful for comparing objects by value.
Parameters:
| Name | Type | Description |
|---|---|---|
obj |
any |
The value to hash |
exclude |
any[] |
Optional list of values to exclude from hashing |
import { HashObject } from '@cleverbrush/deep';
const hash = HashObject({ name: 'John', age: 30 });
Date instances, array order), deep merge, flattening, and hashing.github/workflows/ci.ymlBSD-3-Clause