fast-equals

repository·main·Indexed 20 days ago

https://github.com/planttheidea/fast-equals

A high-performance, zero-dependency equality comparison utility for JavaScript and TypeScript. It supports various comparison modes including deep, shallow, strict, and circular equality. The library handles a wide range of types such as plain objects, arrays, Maps, Sets, Dates, RegExps, and custom class instances. It includes specialized methods like sameValueEqual and sameValueZeroEqual, as well as a createCustomEqual factory for building bespoke equality comparators.

Tokens
11.9K
Snippets
38
Records
43
Agent score
68%

What's inside fast-equals

  1. Overview of fast-equals

    main

    fast-equals is a lightweight (~2kB minified/gzipped), dependency-free library designed for high-performance equality comparisons between objects. It supports various comparison strategies including deep, shallow, SameValue, SameValueZero, and strict equality, with optional support for circular references and strict property definition checks.

    Supported types out-of-the-box:

    • Plain objects (including react elements and Arguments)
    • Arrays
    • ArrayBuffer / TypedArray / DataView instances
    • Date objects
    • RegExp objects
    • Map / Set iterables
    • Promise objects and then-ables
    • Primitive wrappers (new Boolean(), new Number(), new String())
    • Custom class instances (including subclasses of native classes)
  2. Quickstart: Use deepEqual

    main

    To perform a deep equality comparison between two objects, import deepEqual from fast-equals and pass the two objects as arguments. This is the most common way to check if two objects have the same structure and values recursively.

    import { deepEqual } from 'fast-equals';
    
    console.log(deepEqual({ foo: 'bar' }, { foo: 'bar' })); // true
  3. Support legacy environments without WeakMap

    main

    Starting in version 4.x.x, fast-equals requires WeakMap to be available in the environment for circular equality checks. If you are targeting legacy environments (like IE11) where WeakMap is unavailable and polyfilling is not an option, you can implement a custom comparator using createCustomEqual.

    To do this, you must provide a createState function that returns a custom cache implementation adhering to the Cache contract (implementing get, set, and delete methods). This allows you to perform circular equality checks using a manual array-based or alternative cache mechanism instead of relying on the global WeakMap.

    import { createCustomEqual, sameValueEqual } from 'fast-equals';
    import type { Cache } from 'fast-equals';
    
    // 1. Implement a custom cache that follows the Cache contract
    function getCache(): Cache<any, any> {
      const entries: Array<[object, any]> = [];
    
      return {
        delete(key) {
          for (let index = 0; index < entries.length; ++index) {
            if (entries[index][0] === key) {
              entries.splice(index, 1);
              return true;
            }
          }
          return false;
        },
    
        get(key) {
          for (let index = 0; index < entries.length; ++index) {
            if (entries[index][0] === key) {
              return entries[index][1];
            }
          }
        },
    
        set(key, value) {
          for (let index = 0; index < entries.length; ++index) {
            if (entries[index][0] === key) {
              entries[index][1] = value;
              return this;
            }
          }
          entries.push([key, value]);
          return this;
        },
      };
    }
    
    // 2. Use createCustomEqual to inject the custom cache via createState
    const circularDeepEqual = createCustomEqual<Cache>({
      circular: true,
      createState: () => ({
        cache: getCache(),
      }),
    });
    
    // Or with a specific comparator like sameValueEqual
    const circularShallowEqual = createCustomEqual<Cache>({
      circular: true,
      comparator: sameValueEqual,
      createState: () => ({
        cache: getCache(),
      }),
    });
  4. Support legacy environments for RegExp comparators

    main

    In environments that lack support for RegExp.prototype.flags (such as IE11), fast-equals may fail to compare regular expressions correctly. If you cannot polyfill RegExp.prototype.flags, you can provide a custom comparator via createCustomEqual that manually checks all individual RegExp properties (source, global, ignoreCase, multiline, unicode, sticky, and lastIndex).

    import { createCustomEqual, sameValueEqual } from 'deep-Equals';
    
    const areRegExpsEqual = (a: RegExp, b: RegExp) =>
      a.source === b.source
      && a.global === b.global
      && a.ignoreCase === b.ignoreCase
      && a.multiline === b.multiline
      && a.unicode === b.unicode
      && a.sticky === b.sticky
      && a.lastIndex === b.lastIndex;
    
    const deepEqual = createCustomEqual({
      createCustomConfig: () => ({ areRegExpsEqual }),
    });
    const shallowEqual = createCustomEqual({
      comparator: sameValueEqual,
      createCustomConfig: () => ({ areRegExpsEqual }),
    });
  5. Build the project artifacts

    main

    Run the build command to generate the distribution files.

    Note on Checksums: If you are attempting to match the exact checksum of the versions released on npm, you may need to normalize line endings. On Linux, you can use unix2dos on the output file.

    yarn run build
    
    # Optional: normalize line endings for npm checksum parity
    unix2dos dist/fast-equals.min.js
  6. Reproduce a build from source

    main

    To reproduce a build of fast-equals, clone the repository and checkout the specific version you wish to build. Replace {version} with the target package version. Note that for versions older than 1.6.2, you must use a specific commit hash instead of a version tag.

    git clone https://github.com/planttheidea/fast-equals.git
    cd fast-equals
    git checkout {version}
  7. Compare objects with non-standard properties using createCustomEqual

    main

    When objects require equality checks that extend beyond their own keys, properties, or symbols (e.g., checking values stored in a WeakMap or properties on a prototype), use createCustomEqual to define a custom comparator.

    To implement this, use the createCustomConfig option within createCustomEqual. This allows you to intercept the object comparison logic by providing a custom areObjectsEqual function. This custom function receives the current objects a and b, and the current state, allowing you to wrap the default comparison logic with your own specialized checks.

    import { createCustomEqual } from 'fast-equals';
    import type { EqualityComparator } from 'fast-equals';
    
    // 1. Define your custom logic (e.g., checking WeakMap references or prototype properties)
    function createAreObjectsEqual<AreObjectsEqual extends EqualityComparator<any>>(
      areObjectsEqual: AreObjectsEqual,
    ): AreObjectsEqual {
      return function (a, b, state) {
        // Call the original comparator first
        if (!areObjectsEqual(a, b, state)) {
          return false;
        }
    
        // Add custom logic for specific types/properties
        // Example: checking a property that exists on the prototype or in a WeakMap
        // ...
    
        return true;
      };
    }
    
    // 2. Create the custom equality instance
    const deepEqual = createCustomEqual({
      createCustomConfig: ({ areObjectsEqual }) => ({
        areObjectsEqual: createAreObjectsEqual(areObjectsEqual),
      }),
    });
  8. Optimize performance with explicit property checks

    main

    When deep equality checks become a performance bottleneck, you can bypass the general-purpose recursive comparison by providing a custom equality function. This is useful when you know the exact shape of your objects and can perform highly specific, shallow, or targeted comparisons on known properties.

    To implement this, define a comparison function that takes two objects of your specific type and returns a boolean. Then, use createCustomEqual with a createCustomConfig function that returns an object containing your custom comparison function under the key areObjectsEqual.

    import { createCustomEqual } from 'fast-equals';
    
    interface SpecialObject {
      foo: string;
      bar: {
        baz: number;
      };
    }
    
    // Define a highly specific comparison for your known shape
    const areObjectsEqual = (a: SpecialObject, b: SpecialObject) => 
      a.foo === b.foo && a.bar.baz === b.bar.baz;
    
    // Register the custom comparison via createCustomEqual
    const isSpecialObjectEqual = createCustomEqual({
      createCustomConfig: () => ({ areObjectsEqual }),
    });
  9. Importing specific builds (ESM vs CommonJS)

    main

    While npm typically resolves the correct build automatically, you can manually force a specific build by importing from the following paths:

    • ESM: fast-equals/dist/es/index.mjs
    • CommonJS: fast-equals/dist/cjs/index.cjs
  10. Use custom `meta` values in comparisons with `createCustomEqual`

    main

    When equality checks depend on external state rather than just the object properties themselves, you can use createCustomEqual to inject a meta object into the comparison lifecycle.

    To implement this, provide a createState function that returns your external state (the meta object) and a createInternalComparator function. The createInternalComparator receives a state argument which contains the object returned by createState. You can then use this state within your comparison logic to influence the result.

    import { createCustomEqual } from 'fast-equals';
    
    interface Meta {
      value: string;
    }
    
    const meta: Meta = { value: 'baz' };
    
    const deepEqual = createCustomEqual<Meta>({
      createInternalComparator: (compare) => (a, b, _keyA, _keyB, _parentA, _parentB, state) =>
        compare(a, b, state) || a === state.meta.value || b === state.meta.value,
      createState: () => ({ meta }),
    });
  11. Handle custom or unsupported objects with createCustomEqual

    main

    Standard equality comparators in fast-equals rely on Object.prototype.toString.call() to select built-in comparators. If you need to support objects that don't work with this check—such as WeakMap, class instances with a custom Symbol.toStringTag, or new language proposals (like Temporal)—you can use createCustomEqual combined with the getUnsupportedCustomComparator handler.

    To implement this:

    1. Define a custom comparison function for your specific type.
    2. Use createCustomEqual and provide a createCustomConfig function.
    3. Inside createCustomConfig, implement getUnsupportedCustomComparator to intercept the object (e.g., by checking its Symbol.toStringTag) and return your custom comparator.
    import { createCustomEqual } from 'fast-equals';
    
    // 1. Define the custom comparison logic
    const areZonedDateTimesEqual = (a: unknown, b: unknown) =>
      a instanceof Temporal.ZonedDateTime && b instanceof Temporal.ZonedDateTime && a.equals(b);
    
    // 2. Create a new comparator using the handler
    const isSpecialObjectEqual = createCustomEqual({
      createCustomConfig: () => ({
        getUnsupportedCustomComparator(a) {
          // 3. Check for the specific type/tag
          if (a?.[Symbol.toStringTag] === 'Temporal.ZonedDateTime') {
            return areZonedDateTimesEqual;
          }
        },
      }),
    });