Deep Equality For Maps Sets And Cycles

Compare nested values by structure, including dates, regexes, unordered maps and sets, with cycle detection and no JSON serialization.

TypescriptData Integrity

Code

snippet
function isObject(value: unknown): value is Record<PropertyKey, unknown> {
  return value !== null && typeof value === "object";
}

export function deepEqual(left: unknown, right: unknown): boolean {
  return compare(left, right, new WeakMap<object, WeakSet<object>>());
}

function compare(
  left: unknown,
  right: unknown,
  seen: WeakMap<object, WeakSet<object>>,
): boolean {
  if (Object.is(left, right)) return true;
  if (!isObject(left) || !isObject(right)) return false;
  if (Object.getPrototypeOf(left) !== Object.getPrototypeOf(right)) return false;

  let partners = seen.get(left);
  if (partners?.has(right)) return true;
  if (!partners) {
    partners = new WeakSet<object>();
    seen.set(left, partners);
  }
  partners.add(right);

  if (left instanceof Date && right instanceof Date) {
    return Object.is(left.getTime(), right.getTime());
  }
  if (left instanceof RegExp && right instanceof RegExp) {
    return left.source === right.source && left.flags === right.flags;
  }
  if (left instanceof Map && right instanceof Map) {
    if (left.size !== right.size) return false;
    for (const [key, value] of left) {
      let found = false;
      for (const [otherKey, otherValue] of right) {
        if (compare(key, otherKey, seen) && compare(value, otherValue, seen)) {
          found = true;
          break;
        }
      }
      if (!found) return false;
    }
    return true;
  }
  if (left instanceof Set && right instanceof Set) {
    if (left.size !== right.size) return false;
    for (const value of left) {
      let found = false;
      for (const other of right) {
        if (compare(value, other, seen)) {
          found = true;
          break;
        }
      }
      if (!found) return false;
    }
    return true;
  }
  if (Array.isArray(left) || Array.isArray(right)) {
    if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length) {
      return false;
    }
    for (let index = 0; index < left.length; index += 1) {
      if (!compare(left[index], right[index], seen)) return false;
    }
    return true;
  }

  const leftKeys = Reflect.ownKeys(left);
  const rightKeys = Reflect.ownKeys(right);
  if (leftKeys.length !== rightKeys.length) return false;
  for (const key of leftKeys) {
    if (!Object.prototype.hasOwnProperty.call(right, key)) return false;
    if (!compare(left[key], right[key], seen)) return false;
  }
  return true;
}