Crafting a Custom toEqual Matcher for Jest
Jest's toEqual matcher is a cornerstone for asserting deep equality between objects and arrays. This challenge asks you to implement your own version of toEqual in TypeScript, providing a deeper understanding of how Jest matchers work and allowing you to customize equality checks for specific data structures. This exercise will strengthen your TypeScript skills and your understanding of Jest's internal workings.
Problem Description
Your task is to create a TypeScript function that mimics the behavior of Jest's toEqual matcher. This function should take two arguments, expected and actual, and return true if they are deeply equal, and false otherwise. Deep equality means that for objects, all corresponding properties must have the same values (recursively), and for arrays, all elements at the same index must be deeply equal.
Key Requirements:
- Recursive Comparison: The function must recursively compare nested objects and arrays.
- Type Safety: The function should be written in TypeScript and leverage type checking to ensure correctness.
- Handles Primitive Types: The function should correctly handle primitive types (string, number, boolean, null, undefined, symbol).
- Handles Circular References: The function must detect and handle circular references to prevent infinite loops. If a circular reference is detected, return
false. - Handles
NaN:NaNshould not be considered equal to itself.
Expected Behavior:
The function should return true if the expected and actual values are deeply equal. Otherwise, it should return false.
Edge Cases to Consider:
- Circular references in objects or arrays.
NaNvalues.- Different types of values (e.g., comparing a number to a string).
- Empty objects and arrays.
- Objects with different keys, even if the values are the same.
- Arrays with different lengths.
Examples
Example 1:
Input: expected = { a: 1, b: { c: 2 } }, actual = { a: 1, b: { c: 2 } }
Output: true
Explanation: Both objects have the same keys and values, recursively.
Example 2:
Input: expected = [1, 2, [3, 4]], actual = [1, 2, [3, 4]]
Output: true
Explanation: Both arrays have the same elements, recursively.
Example 3:
Input: expected = { a: 1, b: { c: 2 } }, actual = { a: 1, b: { c: 3 } }
Output: false
Explanation: The value of 'b.c' is different.
Example 4: (Circular Reference)
Input: expected = { a: 1 }, actual = expected
Output: false
Explanation: Circular reference detected.
Example 5: (NaN)
Input: expected = NaN, actual = NaN
Output: false
Explanation: NaN is not equal to itself.
Constraints
- The function must be written in TypeScript.
- The function must handle objects and arrays of arbitrary depth.
- The function must not throw errors.
- The function should be reasonably performant; avoid unnecessary computations. While performance isn't the primary focus, excessively slow solutions will be considered less ideal.
- The function should correctly handle all primitive types.
Notes
- Consider using recursion to traverse the objects and arrays.
- A
Setcan be helpful for detecting circular references. - Pay close attention to the handling of
NaN. Direct equality comparison (===) will not work correctly forNaN. - Think about how to handle different types of values gracefully. You might want to return
falseimmediately if the types are different. - This is a good opportunity to practice TypeScript's type system and generics.