isDeepEqual function
Checks whether two values are equal at every depth: primitives (NaN equals NaN), arrays, objects with the same prototype, Date, RegExp, Map, Set and typed arrays. Circular references work.
ts
import { isDeepEqual } from 'utils';Signature
ts
export function isDeepEqual(a: unknown, b: unknown): booleanParameters
| Name | Description |
|---|---|
a | The first value. |
b | The second value. |
Returns
true when both values have the same structure and contents.
Example
ts
isDeepEqual({ position: { x: 1 }, tags: ['a'] }, { position: { x: 1 }, tags: ['a'] }); // trueTests
Line coverage: 100 %.
- ✅ finds 1 and 1 equal
- ✅ finds null and null equal
- ✅ finds 0 and 0 equal
- ✅ finds "a" and "a" equal
- ✅ finds null and null equal
- ✅ finds {"a":{"b":[1,2]}} and {"a":{"b":[1,2]}} equal
- ✅ finds [1,[2,[3]]] and [1,[2,[3]]] equal
- ✅ finds "1970-01-01T00:00:00.005Z" and "1970-01-01T00:00:00.005Z" equal
- ✅ finds {} and {} equal
- ✅ finds {} and {} equal
- ✅ finds {} and {} equal
- ✅ finds {"0":1,"1":2} and {"0":1,"1":2} equal
- ✅ finds {"x":1,"y":2} and {"x":1,"y":2} equal
- ✅ finds 1 and 2 different
- ✅ finds 1 and "1" different
- ✅ finds null and undefined different
- ✅ finds {} and null different
- ✅ finds {"a":1} and {"a":2} different
- ✅ finds {"a":1} and {"a":1} different
- ✅ finds {"a":1,"c":2} and {"a":1,"b":2} different
- ✅ finds [1,2] and [2,1] different
- ✅ finds [1] and {"0":1} different
- ✅ finds "1970-01-01T00:00:00.005Z" and "1970-01-01T00:00:00.006Z" different
- ✅ finds {} and {} different
- ✅ finds {} and {} different
- ✅ finds {} and {} different
- ✅ finds {} and {} different
- ✅ finds {} and {} different
- ✅ finds {} and {} different
- ✅ finds {} and {} different
- ✅ finds {"0":1,"1":2} and {"0":1,"1":3} different
- ✅ finds {"0":1} and {"0":1,"1":2} different
- ✅ finds {"x":1,"y":2} and {"x":1,"y":2} different
- ✅ supports circular references
Source
src/object/is-deep-equal.ts (on GitHub)
Uses
isObject: Checks whether a value is a non-null object: plain object, array, class instance, date…isRecordaccepts plain objects only.
Files it needs
src/object/is-deep-equal.ts imports, directly or not:
Source code
ts
import { isObject } from '../guard';
type Visited = WeakMap<object, WeakSet<object>>;
function equalObjects(a: object, b: object, visited: Visited): boolean {
if (a instanceof Date && b instanceof Date) {
return Object.is(a.getTime(), b.getTime());
}
if (a instanceof RegExp && b instanceof RegExp) {
return a.source === b.source && a.flags === b.flags;
}
if (a instanceof Set && b instanceof Set) {
return a.size === b.size && [...a].every((item: unknown) => b.has(item));
}
if (a instanceof Map && b instanceof Map) {
return (
a.size === b.size &&
[...a].every(([key, value]: readonly [unknown, unknown]) => b.has(key) && equals(value, b.get(key), visited))
);
}
if (ArrayBuffer.isView(a) && ArrayBuffer.isView(b)) {
const first = new Uint8Array(a.buffer, a.byteOffset, a.byteLength);
const second = new Uint8Array(b.buffer, b.byteOffset, b.byteLength);
return first.length === second.length && first.every((byte, index) => byte === second[index]);
}
const keys = Object.keys(a);
return (
keys.length === Object.keys(b).length &&
keys.every((key) => Object.hasOwn(b, key) && equals(Reflect.get(a, key), Reflect.get(b, key), visited))
);
}
function equals(a: unknown, b: unknown, visited: Visited): boolean {
if (a === b || (Number.isNaN(a) && Number.isNaN(b))) {
return true;
}
if (!isObject(a) || !isObject(b) || Object.getPrototypeOf(a) !== Object.getPrototypeOf(b)) {
return false;
}
const seen = visited.get(a);
if (seen?.has(b) === true) {
return true;
}
visited.set(a, (seen ?? new WeakSet()).add(b));
return equalObjects(a, b, visited);
}
/**
* Checks whether two values are equal at every depth: primitives (`NaN` equals `NaN`), arrays, objects with the same
* prototype, `Date`, `RegExp`, `Map`, `Set` and typed arrays. Circular references work.
*
* @param a - The first value.
* @param b - The second value.
* @returns `true` when both values have the same structure and contents.
* @example
* isDeepEqual({ position: { x: 1 }, tags: ['a'] }, { position: { x: 1 }, tags: ['a'] }); // true
*/
export function isDeepEqual(a: unknown, b: unknown): boolean {
return equals(a, b, new WeakMap());
}Test file
ts
import { isDeepEqual } from './is-deep-equal';
class Point {
public readonly x: number;
public readonly y: number;
public constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
}
describe(isDeepEqual, () => {
it.for([
[1, 1],
[NaN, NaN],
[0, -0],
['a', 'a'],
[null, null],
[{ a: { b: [1, 2] } }, { a: { b: [1, 2] } }],
[
[1, [2, [3]]],
[1, [2, [3]]],
],
[new Date(5), new Date(5)],
[/a/giv, /a/giv],
[new Set([1, 2]), new Set([2, 1])],
[new Map([['k', { v: 1 }]]), new Map([['k', { v: 1 }]])],
[new Float64Array([1, 2]), new Float64Array([1, 2])],
[new Point(1, 2), new Point(1, 2)],
])('finds %j and %j equal', ([a, b]) => {
expect(isDeepEqual(a, b)).toBe(true);
});
it.for([
[1, 2],
[1, '1'],
[null, undefined],
[{}, null],
[{ a: 1 }, { a: 2 }],
[{ a: 1 }, { a: 1, b: undefined }],
[
{ a: 1, c: 2 },
{ a: 1, b: 2 },
],
[
[1, 2],
[2, 1],
],
[[1], { 0: 1 }],
[new Date(5), new Date(6)],
[/a/v, /a/iv],
[/a/v, /b/v],
[new Set([1]), new Set([2])],
[new Set([1]), new Set([1, 2])],
[new Map([['k', 1]]), new Map([['k', 2]])],
[new Map([['k', 1]]), new Map([['j', 1]])],
[new Map([['k', 1]]), new Map()],
[new Uint8Array([1, 2]), new Uint8Array([1, 3])],
[new Uint8Array([1]), new Uint8Array([1, 2])],
[new Point(1, 2), { x: 1, y: 2 }],
])('finds %j and %j different', ([a, b]) => {
expect(isDeepEqual(a, b)).toBe(false);
});
it('supports circular references', () => {
const a: { self?: unknown; value: number } = { value: 1 };
a.self = a;
const b: { self?: unknown; value: number } = { value: 1 };
b.self = b;
expect(isDeepEqual(a, b)).toBe(true);
expect(isDeepEqual([a, a], [b, b])).toBe(true);
});
});Keywords: is, deep, equal, objects, function