Skip to content

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): boolean

Parameters ​

NameDescription
aThe first value.
bThe 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'] }); // true

Tests ​

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… isRecord accepts 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

Generated from the JSDoc of the sources.