Deep equality comparison library. Correctness and comprehensive type support over raw performance. Single export:
isEqual(a, b).
| Command | Description |
|---|---|
yarn test |
Run tests (vitest through vp) |
yarn test:coverage |
Tests with coverage report |
yarn lint |
Oxlint |
yarn lint:fix |
Oxlint auto-fix |
yarn fmt |
Format with oxfmt |
yarn fmt:check |
Check formatting |
yarn typecheck |
Type check without emitting |
yarn build |
Clean + compile TypeScript |
yarn benchmark |
Run vitest benchmarks |
vite.config.ts # Vite+ config β test, lint and fmt in one place
src/
is-equal.ts # Entire implementation (~157 lines, single exported function)
is-equal.test.ts # Test runner β iterates fixture suites + edge case tests
comparison.benchmark.ts # Vitest benchmarks vs other libraries
fixtures/
tests.ts # Test case definitions (TestSuite[] with TestCase[])
benchmark.ts # Benchmark fixtures
Single-file library. All comparison logic lives in src/is-equal.ts as one module-level recursive function.
- Yarn 4 β
yarn@4.17.1, node-modules linker - TypeScript 7 β native compiler, ESNext target, NodeNext modules
- Strict beyond
strictβnoUncheckedIndexedAccess,exactOptionalPropertyTypes,verbatimModuleSyntax,erasableSyntaxOnly; the build emits declarations underisolatedDeclarations - ESM only β
"type": "module",.jsextensions in imports - Vite+ toolchain β
vpdrives vitest, oxlint and oxfmt; everything is configured invite.config.ts @ver0/oxlint-configβjavascript,typescript+typescriptUnsafe,nodeandvitestpresets. The unsafe preset disables theno-unsafe-*family (intentional, the core function usesany)- Conventional commits β
feat,fix,perf,refactor,test,chore,docs - Semantic release β automated versioning, do not manually bump versions
- Reverse iteration in hot loops:
for (let i = length; i-- !== 0;) - Self-comparison for NaN:
a !== a && b !== b(faster thanNumber.isNaN) - Prototype-based type checking:
Object.getPrototypeOf(a) !== Object.getPrototypeOf(b)before constructor checks - Variable reuse: parameters
a/bmay be reassigned locally β intentional, not a bug - Cached prototype methods:
const {valueOf, toString} = Object.prototypeat module scope - oxlint-disable comments are intentional β
typescript/unbound-methodon the prototype method cache,typescript/no-restricted-typeson theWeakMap<object, object>signature,no-self-compareon the NaN checks
- Fixture-driven: Tests defined as
TestSuite[]insrc/fixtures/tests.ts, iterated by the test runner - Bidirectional: Every test case runs with both
(a, b)and(b, a)argument order - Adding tests: Add
TestCaseentries to the appropriate suite insrc/fixtures/tests.tsβ include both equal and not-equal cases - Edge cases: Standalone
it()blocks inis-equal.test.tsfor circular refs, null-prototype, NaN
- Sets use reference equality β
new Set([{a:1}])vsnew Set([{a:1}])returnsfalse. Intentional β respects the Set's own SameValueZero identity model rather than overriding it with deep comparison - Lazy WeakMap β created only when recursion into objects/arrays/maps occurs. Primitives, Date, RegExp, Set, and TypedArray comparisons never allocate it
- Stack depth β recursive algorithm, deep nesting (>1000 levels) may cause stack overflow. Not mitigated; rare in practice
- Symbol-keyed properties ignored β symbols are designed as non-enumerable hidden identifiers for metadata
(well-known symbols,
$$typeof), not data-carrying properties. Comparing them as data would contradict their intended role - Custom classes β compared via
valueOf()thentoString()fallback, only when both instances share the same function reference. Classes without these return false for different instances with same data - TypedArray byte comparison β all TypedArrays and DataViews compared via Uint8Array over their
byteOffset/byteLengthslice. Byte-level comparison preserves NaN bit patterns
fix:β patch releasefeat:β minor releaseBREAKING CHANGE:footer β major release
Breaking changes MUST use BREAKING CHANGE: (two words, uppercase) as a git trailer in the commit footer.
BREAKING-CHANGE: is also accepted.
Do NOT use BREAKING: alone or ! in the subject β the Angular preset does not detect these and the major version bump
will be silently skipped.