Test deep equality with arrays and objects
Build deep equality with arrays and objects. The interviewer expects a small, reusable utility with clear behavior under repeated calls and invalid inputs.
Answer Strategy
For deep equality with arrays and objects, start by stating the public contract before writing code: argument shape, return shape, mutation rules, error behavior, and whether work is synchronous, timed, cached, or cancellable.
A senior solution uses boring names for hidden state. If the function stores a timer, cache entry, listener, or in-flight promise, say who owns that state and how it is cleaned up.
After the baseline passes, harden the edge cases: empty input, repeated calls, invalid values, thrown callbacks, stable ordering, and memory lifetime. The reference below is written to be narrated line by line.
Reference Implementation: Deep Equality
Deep equality is mostly about agreeing on scope: primitives, arrays, object keys, dates, and cyclic data.
function deepEqual(a: unknown, b: unknown, seen = new WeakMap<object, object>()): boolean {
if (Object.is(a, b)) return true;
if (a === null || b === null || typeof a !== 'object' || typeof b !== 'object') {
return false;
}
const cached = seen.get(a as object);
if (cached === b) return true;
seen.set(a as object, b as object);
if (a instanceof Date || b instanceof Date) {
return a instanceof Date && b instanceof Date && a.getTime() === b.getTime();
}
if (Array.isArray(a) || Array.isArray(b)) {
if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false;
return a.every((item, index) => deepEqual(item, b[index], seen));
}
const aKeys = Object.keys(a as Record<string, unknown>);
const bKeys = Object.keys(b as Record<string, unknown>);
if (aKeys.length !== bKeys.length) return false;
return aKeys.every((key) =>
Object.prototype.hasOwnProperty.call(b, key) &&
deepEqual((a as Record<string, unknown>)[key], (b as Record<string, unknown>)[key], seen)
);
}Runnable Playground
Edit the implementation and run the tests directly in the browser. For system design questions, the playground focuses on the core state/data logic that the UI would call.
function deepEqual(a: unknown, b: unknown, seen = new WeakMap<object, object>()): boolean {
if (Object.is(a, b)) return true;
if (a === null || b === null || typeof a !== 'object' || typeof b !== 'object') {
return false;
}
const cached = seen.get(a as object);
if (cached === b) return true;
seen.set(a as object, b as object);
if (a instanceof Date || b instanceof Date) {
return a instanceof Date && b instanceof Date && a.getTime() === b.getTime();
}
if (Array.isArray(a) || Array.isArray(b)) {
if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false;
return a.every((item, index) => deepEqual(item, b[index], seen));
}
const aKeys = Object.keys(a as Record<string, unknown>);
const bKeys = Object.keys(b as Record<string, unknown>);
if (aKeys.length !== bKeys.length) return false;
return aKeys.every((key) =>
Object.prototype.hasOwnProperty.call(b, key) &&
deepEqual((a as Record<string, unknown>)[key], (b as Record<string, unknown>)[key], seen)
);
}Testing Strategy
Convert the answer into observable behavior. In a mid-senior interview, say which behaviors are covered by unit tests, interaction tests, accessibility checks, and one browser smoke path.
test('deepEqual compares nested objects and handles cycles', () => {
const a: any = { ids: [1, 2], meta: { active: true } };
const b: any = { ids: [1, 2], meta: { active: true } };
a.self = a;
b.self = b;
expect(deepEqual(a, b)).toBe(true);
expect(deepEqual(a, { ids: [2, 1], meta: { active: true } })).toBe(false);
});Interviewer Signal
Tests whether you can turn a familiar utility into a precise contract instead of coding only the happy path.
Constraints
- Define the function signature before coding.
- Do not rely on global mutable state unless it is part of the returned closure.
- Explain time and space cost for the common path.
Model Answer Shape
- Write the smallest public contract first.
- Cover empty input, repeated calls, thrown errors, and cleanup behavior.
- Keep implementation readable enough to narrate under interview pressure.
Tradeoffs
- A compact implementation is attractive, but explicit state names are easier to debug live.
- Supporting every possible input can distract from the core contract; state the scope before coding.
Edge Cases
- No arguments or undefined callbacks.
- Synchronous throw inside the wrapped function.
- Repeated calls before the previous result settles.
Testing And Proof
- Happy path with representative inputs.
- Boundary input and repeated invocation.
- Cleanup or cancellation if timers or promises are involved.
Follow-Ups
- How would you expose cancellation?
- How would the API change for React usage?