fast-equals v5.2.2
fast-equals
Perform blazing fast equality comparisons (either deep or shallow) on two objects passed, while also maintaining a high degree of flexibility for various implementation use-cases. It has no dependencies, and is ~1.8kB when minified and gzipped.
The following types are handled out-of-the-box:
- Plain objects (including
reactelements andArguments) - Arrays
- Typed Arrays
DateobjectsRegExpobjectsMap/SetiterablesPromiseobjects- Primitive wrappers (
new Boolean()/new Number()/new String()) - Custom class instances, including subclasses of native classes
Methods are available for deep, shallow, or referential equality comparison. In addition, you can opt into support for circular objects, or performing a "strict" comparison with unconventional property definition, or both. You can also customize any specific type comparison based on your application's use-cases.
Table of contents
Usage
import { deepEqual } from 'fast-equals';
console.log(deepEqual({ foo: 'bar' }, { foo: 'bar' })); // trueSpecific builds
By default, npm should resolve the correct build of the package based on your consumption (ESM vs CommonJS). However, if you want to force use of a specific build, they can be located here:
- ESM =>
fast-equals/dist/esm/index.mjs - CommonJS =>
fast-equals/dist/cjs/index.cjs - UMD =>
fast-equals/dist/umd/index.js - Minified UMD =>
fast-equals/dist/min/index.js
If you are having issues loading a specific build type, please file an issue.
Available methods
deepEqual
Performs a deep equality comparison on the two objects passed and returns a boolean representing the value equivalency of the objects.
import { deepEqual } from 'fast-equals';
const objectA = { foo: { bar: 'baz' } };
const objectB = { foo: { bar: 'baz' } };
console.log(objectA === objectB); // false
console.log(deepEqual(objectA, objectB)); // trueComparing Maps
Map objects support complex keys (objects, Arrays, etc.), however the spec for key lookups in Map are based on SameZeroValue. If the spec were followed for comparison, the following would always be false:
const mapA = new Map([[{ foo: 'bar' }, { baz: 'quz' }]]);
const mapB = new Map([[{ foo: 'bar' }, { baz: 'quz' }]]);
deepEqual(mapA, mapB);To support true deep equality of all contents, fast-equals will perform a deep equality comparison for key and value parirs. Therefore, the above would be true.
shallowEqual
Performs a shallow equality comparison on the two objects passed and returns a boolean representing the value equivalency of the objects.
import { shallowEqual } from 'fast-equals';
const nestedObject = { bar: 'baz' };
const objectA = { foo: nestedObject };
const objectB = { foo: nestedObject };
const objectC = { foo: { bar: 'baz' } };
console.log(objectA === objectB); // false
console.log(shallowEqual(objectA, objectB)); // true
console.log(shallowEqual(objectA, objectC)); // falsesameValueZeroEqual
Performs a SameValueZero comparison on the two objects passed and returns a boolean representing the value equivalency of the objects. In simple terms, this means either strictly equal or both NaN.
import { sameValueZeroEqual } from 'fast-equals';
const mainObject = { foo: NaN, bar: 'baz' };
const objectA = 'baz';
const objectB = NaN;
const objectC = { foo: NaN, bar: 'baz' };
console.log(sameValueZeroEqual(mainObject.bar, objectA)); // true
console.log(sameValueZeroEqual(mainObject.foo, objectB)); // true
console.log(sameValueZeroEqual(mainObject, objectC)); // falsecircularDeepEqual
Performs the same comparison as deepEqual but supports circular objects. It is slower than deepEqual, so only use if you know circular objects are present.
function Circular(value) {
this.me = {
deeply: {
nested: {
reference: this,
},
},
value,
};
}
console.log(circularDeepEqual(new Circular('foo'), new Circular('foo'))); // true
console.log(circularDeepEqual(new Circular('foo'), new Circular('bar'))); // falseJust as with deepEqual, both keys and values are compared for deep equality.
circularShallowEqual
Performs the same comparison as shallowequal but supports circular objects. It is slower than shallowEqual, so only use if you know circular objects are present.
const array = ['foo'];
array.push(array);
console.log(circularShallowEqual(array, ['foo', array])); // true
console.log(circularShallowEqual(array, [array])); // falsestrictDeepEqual
Performs the same comparison as deepEqual but performs a strict comparison of the objects. In this includes:
- Checking symbol properties
- Checking non-enumerable properties in object comparisons
- Checking full descriptor of properties on the object to match
- Checking non-index properties on arrays
- Checking non-key properties on
Map/Setobjects
const array = [{ foo: 'bar' }];
const otherArray = [{ foo: 'bar' }];
array.bar = 'baz';
otherArray.bar = 'baz';
console.log(strictDeepEqual(array, otherArray)); // true;
console.log(strictDeepEqual(array, [{ foo: 'bar' }])); // false;strictShallowEqual
Performs the same comparison as shallowEqual but performs a strict comparison of the objects. In this includes:
- Checking non-enumerable properties in object comparisons
- Checking full descriptor of properties on the object to match
- Checking non-index properties on arrays
- Checking non-key properties on
Map/Setobjects
const array = ['foo'];
const otherArray = ['foo'];
array.bar = 'baz';
otherArray.bar = 'baz';
console.log(strictDeepEqual(array, otherArray)); // true;
console.log(strictDeepEqual(array, ['foo'])); // false;strictCircularDeepEqual
Performs the same comparison as circularDeepEqual but performs a strict comparison of the objects. In this includes:
- Checking
Symbolproperties on the object - Checking non-enumerable properties in object comparisons
- Checking full descriptor of properties on the object to match
- Checking non-index properties on arrays
- Checking non-key properties on
Map/Setobjects
function Circular(value) {
this.me = {
deeply: {
nested: {
reference: this,
},
},
value,
};
}
const first = new Circular('foo');
Object.defineProperty(first, 'bar', {
enumerable: false,
value: 'baz',
});
const second = new Circular('foo');
Object.defineProperty(second, 'bar', {
enumerable: false,
value: 'baz',
});
console.log(circularDeepEqual(first, second)); // true
console.log(circularDeepEqual(first, new Circular('foo'))); // falsestrictCircularShallowEqual
Performs the same comparison as circularShallowEqual but performs a strict comparison of the objects. In this includes:
- Checking non-enumerable properties in object comparisons
- Checking full descriptor of properties on the object to match
- Checking non-index properties on arrays
- Checking non-key properties on
Map/Setobjects
const array = ['foo'];
const otherArray = ['foo'];
array.push(array);
otherArray.push(otherArray);
array.bar = 'baz';
otherArray.bar = 'baz';
console.log(circularShallowEqual(array, otherArray)); // true
console.log(circularShallowEqual(array, ['foo', array])); // falsecreateCustomEqual
Creates a custom equality comparator that will be used on nested values in the object. Unlike deepEqual and shallowEqual, this is a factory method that receives the default options used internally, and allows you to override the defaults as needed. This is generally for extreme edge-cases, or supporting legacy environments.
The signature is as follows:
interface Cache<Key extends object, Value> {
delete(key: Key): boolean;
get(key: Key): Value | undefined;
set(key: Key, value: any): any;
}
interface ComparatorConfig<Meta> {
areArraysEqual: TypeEqualityComparator<any[], Meta>;
areDatesEqual: TypeEqualityComparator<Date, Meta>;
areErrorsEqual: TypeEqualityComparator<Error, Meta>;
areFunctionsEqual: TypeEqualityComparator<(...args: any[]) => any, Meta>;
areMapsEqual: TypeEqualityComparator<Map<any, any>, Meta>;
areObjectsEqual: TypeEqualityComparator<Record<string, any>, Meta>;
arePrimitiveWrappersEqual: TypeEqualityComparator<
boolean | string | number,
Meta
>;
areRegExpsEqual: TypeEqualityComparator<RegExp, Meta>;
areSetsEqual: TypeEqualityComparator<Set<any>, Meta>;
areTypedArraysEqual: TypeEqualityComparatory<TypedArray, Meta>;
areUrlsEqual: TypeEqualityComparatory<URL, Meta>;
}
function createCustomEqual<Meta>(options: {
circular?: boolean;
createCustomConfig?: (
defaultConfig: ComparatorConfig<Meta>,
) => Partial<ComparatorConfig<Meta>>;
createInternalComparator?: (
compare: <A, B>(a: A, b: B, state: State<Meta>) => boolean,
) => (
a: any,
b: any,
indexOrKeyA: any,
indexOrKeyB: any,
parentA: any,
parentB: any,
state: State<Meta>,
) => boolean;
createState?: () => { cache?: Cache; meta?: Meta };
strict?: boolean;
}): <A, B>(a: A, b: B) => boolean;Create a custom equality comparator. This allows complete control over building a bespoke equality method, in case your use-case requires a higher degree of performance, legacy environment support, or any other non-standard usage. The recipes provide examples of use in different use-cases, but if you have a specific goal in mind and would like assistance feel free to file an issue.
NOTE: Map implementations compare equality for both keys and value. When using a custom comparator and comparing equality of the keys, the iteration index is provided as both indexOrKeyA and indexOrKeyB to help use-cases where ordering of keys matters to equality.
Recipes
Some recipes have been created to provide examples of use-cases for createCustomEqual. Even if not directly applicable to the problem you are solving, they can offer guidance of how to structure your solution.
- Legacy environment support for
RegExpcomparators - Explicit property check
- Using
metain comparison - Comparing non-standard properties
- Strict property descriptor comparison
- Legacy environment support for circualr equal comparators
Benchmarks
All benchmarks were performed on an i9-11900H Ubuntu Linux 24.04 laptop with 64GB of memory using NodeJS version 20.17.0, and are based on averages of running comparisons based deep equality on the following object types:
- Primitives (
String,Number,null,undefined) FunctionObjectArrayDateRegExpreactelements- A mixed object with a combination of all the above types
Testing mixed objects equal...
┌─────────┬─────────────────────────────────┬────────────────┐
│ (index) │ Package │ Ops/sec │
├─────────┼─────────────────────────────────┼────────────────┤
│ 0 │ 'fast-equals' │ 1256867.529926 │
│ 1 │ 'fast-deep-equal' │ 1207041.997437 │
│ 2 │ 'shallow-equal-fuzzy' │ 1142536.391324 │
│ 3 │ 'react-fast-compare' │ 1140373.249605 │
│ 4 │ 'dequal/lite' │ 708240.354044 │
│ 5 │ 'dequal' │ 704655.931143 │
│ 6 │ 'fast-equals (circular)' │ 595853.718756 │
│ 7 │ 'underscore.isEqual' │ 433596.570863 │
│ 8 │ 'assert.deepStrictEqual' │ 310595.198662 │
│ 9 │ 'lodash.isEqual' │ 232192.454526 │
│ 10 │ 'fast-equals (strict)' │ 175941.250843 │
│ 11 │ 'fast-equals (strict circular)' │ 154606.328398 │
│ 12 │ 'deep-eql' │ 136052.484375 │
│ 13 │ 'deep-equal' │ 854.061311 │
└─────────┴─────────────────────────────────┴────────────────┘
Testing mixed objects not equal...
┌─────────┬─────────────────────────────────┬────────────────┐
│ (index) │ Package │ Ops/sec │
├─────────┼─────────────────────────────────┼────────────────┤
│ 0 │ 'fast-equals' │ 3795307.779634 │
│ 1 │ 'fast-deep-equal' │ 2987150.35694 │
│ 2 │ 'react-fast-compare' │ 2733075.404272 │
│ 3 │ 'fast-equals (circular)' │ 2311547.685659 │
│ 4 │ 'dequal/lite' │ 1156909.54415 │
│ 5 │ 'dequal' │ 1151209.161878 │
│ 6 │ 'fast-equals (strict)' │ 1102248.247412 │
│ 7 │ 'fast-equals (strict circular)' │ 1020639.089577 │
│ 8 │ 'nano-equal' │ 1009557.685012 │
│ 9 │ 'underscore.isEqual' │ 770286.698227 │
│ 10 │ 'lodash.isEqual' │ 296338.570457 │
│ 11 │ 'deep-eql' │ 152741.182224 │
│ 12 │ 'assert.deepStrictEqual' │ 20163.203513 │
│ 13 │ 'deep-equal' │ 3519.448516 │
└─────────┴─────────────────────────────────┴────────────────┘Caveats that impact the benchmark (and accuracy of comparison):
Maps,Promises, andSets were excluded from the benchmark entirely because no library other thandeep-eqlfully supported their comparisonfast-deep-equal,react-fast-compareandnano-equalthrow on objects withnullas prototype (Object.create(null))assert.deepStrictEqualdoes not supportNaNorSameValueZeroequality for datesdeep-eqldoes not supportSameValueZeroequality for zero equality (positive and negative zero are not equal)deep-equaldoes not supportNaNand does not strictly compare object type, or date / regexp values, nor usesSameValueZeroequality for datesfast-deep-equaldoes not supportNaNorSameValueZeroequality for datesnano-equaldoes not strictly compare object property structure, array length, or object type, norSameValueZeroequality for datesreact-fast-comparedoes not supportNaNorSameValueZeroequality for dates, and does not comparefunctionequalityshallow-equal-fuzzydoes not strictly compare object type or regexp values, norSameValueZeroequality for datesunderscore.isEqualdoes not supportSameValueZeroequality for primitives or dates
All of these have the potential of inflating the respective library's numbers in comparison to fast-equals, but it was the closest apples-to-apples comparison I could create of a reasonable sample size. It should be noted that react elements can be circular objects, however simple elements are not; I kept the react comparison very basic to allow it to be included.
Development
Standard practice, clone the repo and npm i to get the dependencies. The following npm scripts are available:
- benchmark => run benchmark tests against other equality libraries
- build => build
main,module, andbrowserdistributables withrollup - clean => run
rimrafon thedistfolder - dev => start webpack playground App
- dist => run
build - lint => run ESLint on all files in
srcfolder (also runs ondevscript) - lint:fix => run
lintscript, but with auto-fixer - prepublish:compile => run
lint,test:coverage,transpile:lib,transpile:es, anddistscripts - start => run
dev - test => run AVA with NODE_ENV=test on all files in
testfolder - test:coverage => run same script as
testwith code coverage calculation vianyc - test:watch => run same script as
testbut keep persistent watcher
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
5 years ago
6 years ago
6 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago