All questions

Deep Equal

Premium

Deep Equal

Implement deepEqual(a, b) — a function that returns true when two values look the same all the way down, regardless of whether they share the same reference. Primitives compare by value, arrays and plain objects compare by walking their structure recursively. Read MDN on Object.is and SameValueZero before you start — the question hinges on which equality flavor you pick for primitives.

Signature

function deepEqual(a: unknown, b: unknown): boolean;
// true if a and b are structurally equal across nested objects, arrays,
// primitives, and Dates. false otherwise.

Examples

deepEqual(1, 1);                 // true
deepEqual(NaN, NaN);             // true  — SameValueZero, not ===
deepEqual(0, -0);                // true  — SameValueZero again
deepEqual(null, undefined);      // false — only null equals null
deepEqual({ a: 1, b: 2 }, { b: 2, a: 1 });        // true  — key order doesn't matter
deepEqual([1, 2, 3], [3, 2, 1]);                   // false — array order does matter
deepEqual({ a: [1, { b: 2 }] }, { a: [1, { b: 2 }] }); // true — nested
deepEqual([1, 2], { 0: 1, 1: 2, length: 2 });      // false — array vs object
// Cycles must not blow the stack. Both inputs point at themselves; treat
// them as equal because the structural shape converges.
const a = {}; a.self = a;
const b = {}; b.self = b;
deepEqual(a, b);                 // true

// Dates compare by their numeric timestamp.
deepEqual(new Date(0), new Date(0));   // true
deepEqual(new Date(0), new Date(1));   // false

Notes

  • SameValueZero — primitives use SameValueZero semantics: NaN === NaN is true, and 0 === -0 is true. This matches how Array.prototype.includes and Map/Set key lookup behave, and it's what most "deep equal" libraries (lodash.isEqual, Jest's toEqual) ship.
  • Reference identity short-circuits — if a === b (or Object.is(a, b)), return true immediately. This both speeds up the common case and handles cycle entry, where both branches eventually arrive at the same pair you've already seen.
  • Key-order independence for objects{ a: 1, b: 2 } and { b: 2, a: 1 } are equal. Iterate one side's own enumerable keys and look each up on the other side; check key-set sizes first to catch the "extra key on one side" case.
  • Order-dependence for arrays[1, 2] and [2, 1] are NOT equal. Arrays are positional; compare element-by-element at each index.
  • Type tags must match symmetrically — array vs object is false, Date vs plain object is false even if the object happens to expose the same .getTime(). Check Array.isArray on both sides; check instanceof Date on both sides.
  • Cycle safety — a self-referencing input (a.self = a) must not infinite-recurse. Use a WeakMap to remember <a, b> pairs already in flight; if you revisit one, assume equal (the recursion will terminate at the next non-shared field).
  • Out of scope — you don't need to handle Map, Set, RegExp, typed arrays, Symbol-keyed properties, or class instances with custom equality. Document these as limitations; the "Going further" section covers them.

Unlock the solution & editor

  • Runnable editor + tests

    Solve in the browser with instant Jest feedback.

  • Detailed solutions

    Walkthroughs, edge cases, and complexity notes.

  • Multi-framework variants

    React, Vue, Vanilla, Angular — same question, different stacks.

Upgrade to Premium