fast-equals picks a comparison strategy instead of always going deep
A blazing fast equality comparison, either shallow or deep
At a glance
- What is it?
- fast-equals is a dependency-free TypeScript library for equality comparison, offering deep, shallow, SameValue, SameValueZero and strict variants plus opt-in circular support and custom comparators per type. Its value is that the strictness of a comparison is a choice you make at the call site rather than a behaviour you inherit from a generic deep-equal helper.
- Who is it for?
- fast-equals suits a TypeScript codebase that compares non-trivial objects often enough for a generic deep-equal helper to be on a hot path, and that needs to choose per call site between shallow, deep and spec-exact semantics, including React state comparisons and Map key handling.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 7 days ago.
- What is it written in?
- Mainly TypeScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
A comparison library where the strictness is the API
fast-equals performs equality comparisons between two objects, and the description is unusually blunt about what it is not trying to be. The library has no dependencies and is about 2.65kB minified and gzipped.
The design idea is that equality is not one operation. JavaScript has several, with different rules about NaN and signed zero, and most deep-equal libraries pick one and hide the choice. fast-equals exposes the choice: methods are available for deep, shallow, SameValue, SameValueZero, or strict equality comparison, and on top of that you can opt into support for circular objects, a strict comparison with unconventional property definition, or both, and you can customize any specific type comparison based on your own use cases.
The types handled out of the box are listed in the documentation, and the list is longer than you would expect. Plain objects, including react elements and Arguments, arrays, ArrayBuffer, TypedArray and DataView instances, Date objects, RegExp objects, Map and Set iterables, Promise objects and then-ables, Error objects including subclasses and their own properties, URL objects, primitive wrappers such as new Boolean(), new Number(), new String() and boxed BigInt, and custom class instances including subclasses of native classes.
The audience is a TypeScript or JavaScript team that has outgrown a one-line deep-equal helper and needs to know exactly what a comparison means at each call site.
The base usage is a single import:
import { deepEqual } from 'fast-equals';
console.log(deepEqual({ foo: 'bar' }, { foo: 'bar' })); // trueShallow versus deep, and why the difference shows up in React
The two everyday methods are deepEqual and shallowEqual, and the documentation's examples make the distinction concrete.
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)); // trueshallowEqual does the same for one level, and the example shows the case that matters:
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)); // falseThat last line is the reason shallow comparison exists. objectB and objectC are structurally identical, but objectB shares the same nested reference, so a shallow comparison treats them as different while a deep one treats them as the same. In a React component, choosing between those two is choosing whether a re-render happens.
The library handles react elements explicitly, which is a hint about where it is used most. A shallow comparison that respects reference identity at the second level is exactly the semantics a state comparison hook needs, and the fact that fast-equals treats react elements as a handled type out of the box suggests the author optimised for that case rather than for generic object graphs.
The spec-exact variants, and why NaN keeps showing up
Three of the methods exist to match ECMAScript semantics exactly, and the documentation spells out the rules in each case.
sameValueEqual performs a SameValue comparison. In simple terms, plus zero and minus zero are not equal, NaN is equal to NaN, and all other items are based on referential equality. In environments that support Object.is, the documentation notes that sameValueEqual is just a re-export of that method.
sameValueZeroEqual performs a SameValueZero comparison, where plus zero and minus zero are equal, NaN is equal to NaN, and everything else is referential equality.
strictEqual performs a Strict Equality comparison, where plus zero and minus zero are equal, NaN is not equal to NaN, and everything else is referential equality. The documentation is candid that this is mainly a convenience function, such as needing a default functional equality comparator, and that it is faster to simply compare a === b.
That last note is worth taking at face value. If your comparison is really just referential equality, this library is slower than the operator, and the package.json declares the engine requirement as node >=6.0.0, so it is not exactly a modern-only package either.
The value of the three variants is that they are named, tested, and consistent. When your codebase has a mix of places that need NaN to equal NaN and places that do not, having one library with three clearly named entry points removes a class of subtle bug that a single ad hoc helper invites.
The strictDeepEqual, strictShallowEqual, strictCircularDeepEqual and strictCircularShallowEqual variants combine the strict property definition with the deep and shallow and circular behaviours.
Map keys are compared deeply, which the spec would not do
Map handling is the one place where fast-equals deliberately departs from the language spec, and the documentation is upfront about it.
Map objects support complex keys such as objects and arrays, but the spec for key lookups in Map is based on SameValueZero. If that spec were followed for comparison, two maps with equivalent but distinct object keys would never be equal:
const mapA = new Map([[{ foo: 'bar' }, { baz: 'quz' }]]);
const mapB = new Map([[{ foo: 'bar' }, { baz: 'quz' }]]);
deepEqual(mapA, mapB);That call would return false under spec-following semantics, because the two object keys are different objects. To support true deep equality of all contents, fast-equals performs a deep equality comparison for key and value pairs, so the above returns true.
This is a genuine trade-off rather than a bug. If you rely on Map key identity to mean something, for example a cache keyed by object reference, the deep comparison will report two maps as equal when your code considers their keys different. The reverse risk, that two logically identical maps compare as different, is what the library is fixing.
The Set comparison follows the same path, since the documentation groups Map and Set together as iterables. The Map section is the one that documents the behaviour explicitly, and it is the section to read if your application uses Maps as caches.
Custom comparators for the types your application actually has
Beyond the built-in methods, the documentation describes createCustomEqual for building a comparison that fits your own types, with a section on strict null prototype comparison and a section on getUnsupportedCustomComparator, which is the hook for types the comparator does not know how to handle.
That is the extension point that makes the library usable beyond the built-in list. If your application has a class with its own identity rules, a Date-like value that should compare by a field other than the timestamp, or a type that wraps another value, you register a comparator for it rather than special-casing every call site.
The repository layout backs this up. There is a recipes/ directory at the root, and the documentation's table of contents lists a Recipes section under createCustomEqual, so working examples of custom comparators are shipped rather than left to the reader to invent.
There is also a getUnsupportedCustomComparator hook, which is the interesting name in that list. A comparison that silently treats an unknown type as unequal is a source of quiet bugs, and a hook that lets you decide what to do with an unrecognised type, throw, or fall back to reference equality, makes that decision explicit. The documentation lists it as a named feature rather than burying it, which suggests it is meant to be used.
For anyone who needs the same comparison semantics across several packages, this is the part of the API that scales.
Distribution: ESM, CommonJS and a forced path when npm guesses wrong
The packaging is worth a look because it answers the question that trips people up with small utilities, which build am I actually getting.
By default, npm should resolve the correct build of the package based on consumption, ESM or CommonJS. The package.json exports map is explicit about it, with the root export pointing at import types ./dist/es/index.d.mts and default ./dist/es/index.mjs, and require types ./dist/cjs/index.d.cts and default ./dist/cjs/index.cjs. So both module systems get their own build and their own type declarations, and the resolution happens at install time rather than in your bundler.
If you want to force a specific build, the documentation gives the two paths directly. The ESM build is at fast-equals/dist/es/index.mjs and the CommonJS build is at fast-equals/dist/cjs/index.cjs. That escape hatch matters in the specific case where a bundler or a test runner resolves the wrong condition, and knowing the two paths in advance saves a debugging session.
The published files list is dist, LICENSE, README.md and index.d.ts, and the source lives in src/ with tests in __tests__/. The build tooling is Rollup with Vite, tests run under Vitest with coverage, and the types are checked with the TypeScript compiler in strict configuration. The repository also carries a CHANGELOG.md, an eslint.config.js, a .prettierrc, a BUILD.md for build instructions, and a yarn.lock.
The devDependencies list is unusually informative for a small library, because it names the alternatives the author measured against: deep-eql, deep-equal, dequal, es-toolkit, fast-deep-equal, lodash, nano-equal, react-fast-compare and shallow-equal-fuzzy are all there, along with @types/lodash, @types/ramda and @types/react. Those are the competitors your benchmark should include.
MIT licensed, released three times in two days, benchmarked against everything
The licence is MIT, with the LICENSE file at the repository root, which is the uncomplicated case for a utility of this kind.
Upkeep is fast rather than slow. The release list shows v6.2.0 on 2026-10-14, v6.2.1 on 2026-10-14 and v5.4.5 also on 2026-10-14, and the last push was on 2026-10-16. Three tags on the same day, with two of them on different version lines, looks like a maintenance release to a maintained branch rather than a burst of new features. It also suggests a project where the author ships small corrections promptly.
The version line is worth noting. At v6.x the API has been through enough major versions that the method names and semantics are probably stable, and the CHANGELOG.md at the root is where you would look to confirm what changed between the major lines.
On performance, the documentation links to a benchmarks section and makes a size claim rather than a speed claim for the headline, 2.65kB minified and gzipped with no dependencies. The benchmark tooling in the devDependencies is tinybench, and the config/ and benchmark/ directories at the repository root are where the comparison against lodash and the other libraries is set up. The dev dependency list is the more useful artefact for you: it tells you exactly which comparators the author considered worth measuring against, so your own benchmark should include the same set if you want a fair read.
The remaining caveat is the one the documentation itself raises about strictEqual, that it is faster to compare a === b. fast-equals earns its place where comparisons are structural and frequent, not where they are trivial.
Editorial conclusion
fast-equals suits a TypeScript codebase that compares non-trivial objects often enough for a generic deep-equal helper to be on a hot path, and that needs to choose per call site between shallow, deep and spec-exact semantics, including React state comparisons and Map key handling. It is the wrong choice if you only need referential equality, since the strictEqual wrapper is slower than writing a === b yourself, as the documentation itself notes, and the wrong choice if your objects are circular by design, since the circular variants are documented as slower than deepEqual. Before adopting it, benchmark against the alternatives you already have in your dependency tree, since the devDependencies list shows the comparison libraries the author measured against, and pick the strictness variant deliberately for each call site rather than defaulting everything to deep.
Frequently asked questions
What is the difference between deepEqual and shallowEqual in fast-equals?
deepEqual compares nested values recursively and returns true for two objects with identical nested structure, while shallowEqual compares only one level, so two objects holding the same nested reference are equal but two objects holding structurally identical but distinct nested objects are not. The choice matters in React, where it decides whether a re-render happens.
How does fast-equals compare Maps with object keys?
The Map spec bases key lookups on SameValueZero, which would make two maps with equivalent but distinct object keys unequal. fast-equals deliberately performs a deep equality comparison for key and value pairs instead, so maps with structurally identical object keys and values compare as equal.
Can I define my own comparison for a custom type with fast-equals?
Yes. createCustomEqual builds a comparison for your own types, and the documentation covers strict null prototype comparison plus a getUnsupportedCustomComparator hook for handling types the comparator does not recognise. A recipes directory in the repository ships working examples.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/planttheidea-fast-equals)Was this useful?