Implement Jest's toEqual Matcher in TypeScript
Jest's toEqual matcher is a powerful tool for deeply comparing objects and arrays. It recursively checks all properties of an object or elements of an array, ensuring that the structure and values are identical. This challenge asks you to reimplement a simplified version of this core Jest functionality in TypeScript. Mastering this will give you a deeper understanding of how assertion libraries work and improve your debugging skills.
Problem Description
You need to create a function, let's call it customToEqual, that mimics the behavior of Jest's toEqual matcher. This function will take two arguments: the actual value and the expected value. It should return true if the two values are deeply equal and false otherwise.
Key Requirements:
- Primitive Types: Handle comparison of primitive types (strings, numbers, booleans, null, undefined, symbols, bigints) correctly.
- Object Comparison: For objects, recursively compare their own enumerable properties. The order of properties should not matter.
- Array Comparison: For arrays, compare elements at each index. The arrays must have the same length and all elements must be deeply equal.
- Circular References: Implement basic handling for circular references to prevent infinite recursion.
- Special Object Types: Consider how to handle
DateandRegExpobjects. They should be compared by their value/pattern, not by reference.
Expected Behavior:
customToEqual(5, 5)should returntrue.customToEqual({ a: 1, b: { c: 2 } }, { b: { c: 2 }, a: 1 })should returntrue.customToEqual([1, 2, [3, 4]], [1, 2, [3, 4]])should returntrue.customToEqual({ a: 1 }, { a: 2 })should returnfalse.customToEqual([1, 2], [1, 2, 3])should returnfalse.customToEqual(new Date('2023-10-27'), new Date('2023-10-27'))should returntrue.customToEqual(/abc/, /abc/)should returntrue.
Edge Cases to Consider:
- Comparing different types (e.g.,
customToEqual(5, '5')should befalse). - Comparing
nullandundefined. - Comparing objects with
nullorundefinedproperties. - Comparing empty objects and arrays.
- Handling cases where one argument is an object and the other is a primitive.
- Handling potential circular references within objects/arrays.
Examples
Example 1:
Input:
actual = { name: "Alice", age: 30 }
expected = { age: 30, name: "Alice" }
Output:
true
Explanation: The objects have the same properties and values, regardless of the order.
Example 2:
Input:
actual = [1, { a: 2, b: [3] }]
expected = [1, { b: [3], a: 2 }]
Output:
true
Explanation: The arrays have the same length, and their elements are deeply equal, including nested objects and arrays.
Example 3:
Input:
actual = { a: 1, b: undefined }
expected = { a: 1, b: null }
Output:
false
Explanation: `undefined` and `null` are distinct values.
Example 4 (Circular Reference):
Input:
const obj1: any = { prop: 'value' };
obj1.self = obj1;
const obj2: any = { prop: 'value' };
obj2.self = obj2;
actual = obj1
expected = obj2
Output:
true
Explanation: The objects are structurally identical, including their circular references.
Constraints
- The comparison should be purely structural and value-based. Do not rely on object identity unless comparing primitives or the exact same object instance.
- Your
customToEqualfunction should be implemented in TypeScript. - The implementation should aim for reasonable performance, avoiding unnecessary deep cloning or excessive recursion where possible. However, correctness is prioritized over micro-optimizations.
- You can assume that
DateandRegExpobjects will be instances of their respective classes.
Notes
- Think about how you will track visited objects to detect and handle circular references. A
MaporSetcould be useful here. - When comparing objects, remember to only consider their own enumerable properties.
Object.keys()orObject.getOwnPropertyNames()andObject.getOwnPropertySymbols()might be helpful. - Consider the order of checks: it's generally more efficient to check for primitive equality first before diving into object/array comparisons.
- You can leverage
typeofandinstanceofoperators for type checking. - Consider using
Object.is()for primitive comparisons as it handlesNaNand-0correctly, which are nuances===doesn't always capture perfectly in this context.