Deep Equal in TypeScript: A Comprehensive Guide

In TypeScript, comparing objects and arrays can be a tricky task. Shallow equality checks, which only compare the references of objects, often fall short when you need to determine if two objects have the same internal structure and values. This is where the concept of deep equality comes into play. Deep equality checks recursively compare the properties and values of objects and arrays to determine if they are truly equal. In this blog post, we will explore the fundamental concepts of deep equality in TypeScript, learn how to use it, look at common practices, and discover some best practices.

Table of Contents

  1. Fundamental Concepts of Deep Equal in TypeScript
  2. Usage Methods
  3. Common Practices
  4. Best Practices
  5. Conclusion
  6. References

Fundamental Concepts of Deep Equal in TypeScript

Shallow vs. Deep Equality

  • Shallow Equality: Shallow equality compares the references of two objects. If two objects have the same reference, they are considered equal. For example:
const obj1 = { a: 1 };
const obj2 = obj1;
const obj3 = { a: 1 };

console.log(obj1 === obj2); // true, same reference
console.log(obj1 === obj3); // false, different references
  • Deep Equality: Deep equality checks the values of all properties in an object or array recursively. Two objects are considered deeply equal if they have the same properties with the same values, even if they are different instances.

How Deep Equality Works

Deep equality algorithms typically work by:

  1. Checking if the two values are of the same type.
  2. If they are primitive values (e.g., numbers, strings, booleans), compare them directly.
  3. If they are objects or arrays, recursively compare their properties or elements.

Usage Methods

Implementing a Simple Deep Equal Function

Here is a basic implementation of a deep equal function in TypeScript:

function deepEqual(a: any, b: any): boolean {
    if (a === b) {
        return true;
    }

    if (typeof a!== 'object' || a === null || typeof b!== 'object' || b === null) {
        return false;
    }

    const keysA = Object.keys(a);
    const keysB = Object.keys(b);

    if (keysA.length!== keysB.length) {
        return false;
    }

    for (const key of keysA) {
        if (!keysB.includes(key) ||!deepEqual(a[key], b[key])) {
            return false;
        }
    }

    return true;
}

// Example usage
const objA = { a: 1, b: { c: 2 } };
const objB = { a: 1, b: { c: 2 } };
const objC = { a: 1, b: { c: 3 } };

console.log(deepEqual(objA, objB)); // true
console.log(deepEqual(objA, objC)); // false

Using a Third - Party Library

There are several third - party libraries available that provide deep equal functionality, such as lodash.isEqual. Here is an example of using lodash.isEqual:

import isEqual from 'lodash/isEqual';

const arr1 = [1, [2, 3]];
const arr2 = [1, [2, 3]];
const arr3 = [1, [2, 4]];

console.log(isEqual(arr1, arr2)); // true
console.log(isEqual(arr1, arr3)); // false

Common Practices

Comparing Arrays

When comparing arrays, it’s important to ensure that the order of elements matters if it should. The deep equal function should handle nested arrays correctly. For example:

const nestedArr1 = [1, [2, [3]]];
const nestedArr2 = [1, [2, [3]]];
const nestedArr3 = [1, [2, [4]]];

console.log(deepEqual(nestedArr1, nestedArr2)); // true
console.log(deepEqual(nestedArr1, nestedArr3)); // false

Comparing Objects with Different Property Orders

Deep equal functions should be able to handle objects with properties in different orders. For example:

const objX = { a: 1, b: 2 };
const objY = { b: 2, a: 1 };

console.log(deepEqual(objX, objY)); // true

Best Practices

Consider Performance

Recursive deep equal functions can be computationally expensive, especially for large objects or deeply nested structures. Consider using memoization techniques or more optimized algorithms if performance is a concern.

Handle Circular References

Circular references can cause infinite loops in a deep equal function. You can use a WeakMap to keep track of visited objects and avoid revisiting them. Here is an updated version of the deep equal function that handles circular references:

function deepEqualWithCircular(a: any, b: any, visited = new WeakMap()): boolean {
    if (a === b) {
        return true;
    }

    if (typeof a!== 'object' || a === null || typeof b!== 'object' || b === null) {
        return false;
    }

    if (visited.has(a) && visited.get(a) === b) {
        return true;
    }

    visited.set(a, b);
    visited.set(b, a);

    const keysA = Object.keys(a);
    const keysB = Object.keys(b);

    if (keysA.length!== keysB.length) {
        return false;
    }

    for (const key of keysA) {
        if (!keysB.includes(key) ||!deepEqualWithCircular(a[key], b[key], visited)) {
            return false;
        }
    }

    return true;
}

// Example with circular reference
const circularObj1: any = { a: 1 };
const circularObj2: any = { a: 1 };
circularObj1.self = circularObj1;
circularObj2.self = circularObj2;

console.log(deepEqualWithCircular(circularObj1, circularObj2)); // true

Conclusion

Deep equality is a powerful concept in TypeScript that allows you to compare objects and arrays more accurately than shallow equality. By understanding the fundamental concepts, learning different usage methods, following common practices, and implementing best practices, you can effectively use deep equal functionality in your TypeScript projects. Whether you choose to implement your own deep equal function or use a third - party library, make sure to consider performance and handle edge cases like circular references.

References