npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@mainframework/is-deep-equal

v1.1.0

Published

Deep equality comparison utility with support for objects, arrays, maps, sets, and circular refs

Readme

@mainframework/is-deep-equal

A robust, recursive deep equality checker that handles complex structures including Map, Set, Date, RegExp, Error, ArrayBuffer, DataView, typed arrays, nested objects, circular references, and more — without mutating the input.

Built in TypeScript. Outputs a minified ESM module for JavaScript and TypeScript projects.


Table of Contents


Installation

Using Yarn:

yarn add @mainframework/is-deep-equal

Using npm:

npm install @mainframework/is-deep-equal

Features

  • Deep comparison of primitives, arrays, objects, Map, Set, Date, RegExp, Error, typed arrays, ArrayBuffer, and DataView
  • Handles circular references without infinite loops
  • Order-independent comparison for Map and Set
  • Compares enumerable symbol-keyed object properties
  • Compares NaN as equal to itself
  • Optional normalization of boxed primitives (String, Number, Boolean)
  • Configurable sparse array handling
  • Configurable prototype mismatch tolerance
  • TypeScript support with .d.ts declarations
  • ESM output for modern bundlers

API

isEqual(a: unknown, b: unknown, opts?: EqualityOptions): boolean

| Parameter | Description | |-----------|-------------| | a | First value to compare | | b | Second value to compare | | opts | Optional comparison settings (see Options Reference) |

Returns true if a and b are deeply equal, false otherwise.

interface EqualityOptions {
  strictSparseArrays?: boolean;       // default: false — treat missing slots as distinct from undefined
  normalizeBoxedPrimitives?: boolean; // default: false — unwrap boxed primitives before comparison
  allowPrototypeMismatch?: boolean;   // default: false — ignore prototype chain differences
}

Usage

import { isEqual } from '@mainframework/is-deep-equal';

Exported types are available from the same entry point:

import type { EqualityOptions } from '@mainframework/is-deep-equal';

Supported Types

Primitives

Primitive values are compared with strict equality. NaN compares as equal to itself. Symbols are compared by identity, so Symbol.for (global registry) symbols with the same key are equal, while locally created symbols are not.

isEqual(1, 1)                            // true
isEqual('hello', 'hello')               // true
isEqual(true, true)                     // true
isEqual(null, null)                     // true
isEqual(undefined, undefined)           // true
isEqual(BigInt(42), BigInt(42))         // true
isEqual(Symbol.for('x'), Symbol.for('x')) // true

isEqual(1, 2)                           // false
isEqual(null, undefined)                // false
isEqual(Symbol('x'), Symbol('x'))       // false — different symbol instances

// NaN
isEqual(NaN, NaN)   // true
isEqual(NaN, 0)     // false

Objects

Objects are compared by their enumerable own properties (string-keyed and symbol-keyed). Key insertion order does not matter. By default, objects with different prototypes are not equal.

isEqual({ a: 1, b: 2 }, { b: 2, a: 1 })          // true — order independent
isEqual({ a: { b: { c: 1 } } }, { a: { b: { c: 1 } } }) // true — deeply nested

isEqual({ a: 1 }, { a: 2 })                        // false — different value
isEqual({ a: 1 }, { b: 1 })                        // false — different key

Symbol-keyed properties — enumerable symbol properties are included in the comparison:

const sym = Symbol('key');
isEqual({ [sym]: 42 }, { [sym]: 42 })  // true
isEqual({ [sym]: 1 },  { [sym]: 2 })  // false

// Non-enumerable symbol keys are ignored
const hidden = Symbol('hidden');
const a = {};
Object.defineProperty(a, hidden, { value: 1, enumerable: false });
isEqual(a, {})  // true

Arrays

Arrays are compared element-by-element in order. Nested arrays and mixed-type elements are fully traversed.

isEqual([1, 2, 3], [1, 2, 3])     // true
isEqual([[1], [2]], [[1], [2]])    // true

isEqual([1, 2], [2, 1])           // false — order matters
isEqual([1, 2], [1, 2, 3])        // false — different length

Sparse arrays — by default a missing slot and undefined are treated as equal. Enable strictSparseArrays to distinguish them:

isEqual([1, , 3], [1, undefined, 3])                              // true  (default)
isEqual([1, , 3], [1, undefined, 3], { strictSparseArrays: true }) // false
isEqual([1, , 3], [1, , 3],         { strictSparseArrays: true }) // true

Date

Dates are compared by their UTC time value.

isEqual(new Date('2024-01-01'), new Date('2024-01-01'))  // true
isEqual(new Date('2024-01-01'), new Date('2025-01-01'))  // false

RegExp

Regular expressions are compared by source and flags.

isEqual(/abc/gi, /abc/gi)  // true

isEqual(/abc/,  /def/)     // false — different source
isEqual(/abc/i, /abc/g)    // false — different flags

Error

Errors are compared by name, message, cause (deep), and any enumerable own properties added to the instance.

isEqual(new Error('oops'), new Error('oops'))  // true

// Different error type
isEqual(new TypeError('oops'), new RangeError('oops'))  // false

// Errors with nested cause
const cause = { code: 42 };
isEqual(
  new Error('fail', { cause }),
  new Error('fail', { cause: { code: 42 } })
)  // true

// Errors with custom own properties
const e1 = Object.assign(new Error('oops'), { code: 404 });
const e2 = Object.assign(new Error('oops'), { code: 404 });
const e3 = Object.assign(new Error('oops'), { code: 500 });
isEqual(e1, e2)  // true
isEqual(e1, e3)  // false

Map

Maps are compared by their entries. Insertion order is ignored, and object keys are compared deeply.

// Insertion order does not matter
isEqual(
  new Map([['a', 1], ['b', 2]]),
  new Map([['b', 2], ['a', 1]])
)  // true

// Different values
isEqual(new Map([['a', 1]]), new Map([['a', 2]]))  // false

// Object keys compared deeply
isEqual(
  new Map([[{ x: 1 }, { y: 2 }]]),
  new Map([[{ x: 1 }, { y: 2 }]])
)  // true

// Nested maps
const map1 = new Map([['users', new Map([['alice', { age: 30 }]])]]);
const map2 = new Map([['users', new Map([['alice', { age: 30 }]])]]);
isEqual(map1, map2)  // true

Set

Sets are compared without regard to insertion order. Each element is matched by deep equality.

isEqual(new Set([1, 2, 3]), new Set([3, 1, 2]))  // true — order independent

// Nested sets with object elements
const a = new Set([new Set([{ id: 1 }, { id: 2 }])]);
const b = new Set([new Set([{ id: 1 }, { id: 2 }])]);
isEqual(a, b)  // true

isEqual(new Set([1, 2]), new Set([2, 3]))  // false

Complex nested exampleMap containing a Set with a nested Set and Map of objects:

const set1 = new Set([
  new Set([{ foo: 'bar' }, { baz: 42 }]),
  new Map([['one', { a: 1 }], ['two', { b: 2 }]]),
]);
const set2 = new Set([
  new Set([{ foo: 'bar' }, { baz: 42 }]),
  new Map([['one', { a: 1 }], ['two', { b: 2 }]]),
]);

isEqual(new Map([['data', set1]]), new Map([['data', set2]]))  // true

Typed Arrays

Typed arrays of the same type are compared byte-by-byte. Arrays of different types are not equal even if the underlying bytes match.

isEqual(new Uint8Array([1, 2, 3]), new Uint8Array([1, 2, 3]))   // true
isEqual(new Int16Array([1, 2]),    new Int16Array([1, 2]))       // true

isEqual(new Uint8Array([1, 2]), new Int8Array([1, 2]))           // false — different types
isEqual(new Uint8Array([1, 2]), new Uint8Array([2, 1]))          // false — different values

ArrayBuffer

ArrayBuffers are compared byte-by-byte.

const a = new Uint8Array([1, 2, 3, 4]).buffer;
const b = new Uint8Array([1, 2, 3, 4]).buffer;
isEqual(a, b)  // true

const c = new Uint8Array([1, 2, 3, 5]).buffer;
isEqual(a, c)  // false

DataView

DataViews are compared by the bytes they describe, respecting byteOffset and byteLength.

const bufA = new Uint8Array([0, 10, 20, 30, 40]).buffer;
const bufB = new Uint8Array([0, 10, 20, 30, 40]).buffer;

// Views over the same slice
const a = new DataView(bufA, 1, 4);
const b = new DataView(bufB, 1, 4);
isEqual(a, b)  // true

const bufC = new Uint8Array([0, 10, 20, 30, 99]).buffer;
const c = new DataView(bufC, 1, 4);
isEqual(a, c)  // false

Circular References

Circular references are detected and handled without throwing. Objects that form the same circular structure are considered equal.

const a: any = { foo: {} };
a.foo.bar = a;

const b: any = { foo: {} };
b.foo.bar = b;

isEqual(a, b)  // true

// Mismatched structure
const c: any = { foo: {} };
c.foo.bar = {};
isEqual(a, c)  // false

Known limitation: Structurally non-isomorphic cycles (for example, a two-node mutual reference vs. a self-loop) may incorrectly compare as equal. See Known Limitations.


Boxed Primitives

By default, new String('a') and 'a' are not equal — they have different types. Enable normalizeBoxedPrimitives to unwrap them before comparison.

// Default — no normalization
isEqual(new String('a'),    'a')     // false
isEqual(new Number(1),      1)       // false
isEqual(new Boolean(true),  true)    // false

// With normalization
isEqual(new String('a'),   'a',    { normalizeBoxedPrimitives: true })  // true
isEqual(new Number(1),     1,      { normalizeBoxedPrimitives: true })  // true
isEqual(new Boolean(true), true,   { normalizeBoxedPrimitives: true })  // true

// Two boxed-primitive objects with extra own properties
const s1 = Object.assign(new String('a'), { x: 1 });
const s2 = Object.assign(new String('a'), { x: 1 });
isEqual(s1, s2)  // true

const s3 = Object.assign(new String('a'), { x: 2 });
isEqual(s1, s3)  // false

Prototype Mismatch

By default, objects with different prototypes are not equal, even if they have the same shape. Set allowPrototypeMismatch: true to ignore prototype differences.

class A { x = 1; }
class B { x = 1; }

isEqual(new A(), new B())                              // false — different prototypes
isEqual(new A(), new B(), { allowPrototypeMismatch: true })  // true

// Object with no prototype vs plain object
const noProto = Object.create(null);
noProto.x = 1;

isEqual(noProto, { x: 1 })                              // false
isEqual(noProto, { x: 1 }, { allowPrototypeMismatch: true })  // true

Functions

Functions are compared by reference only. Two functions with identical source code but different identities are not equal.

const fn = () => {};
isEqual(fn, fn)        // true  — same reference

const fn2 = () => {};
isEqual(fn, fn2)       // false — different references

WeakMap, WeakSet, Promise

WeakMap, WeakSet, and Promise cannot be iterated or inspected for equality, so any comparison involving them always returns false.

isEqual(new WeakMap(),       new WeakMap())       // false
isEqual(new WeakSet(),       new WeakSet())       // false
isEqual(Promise.resolve(1),  Promise.resolve(1))  // false

Options Reference

| Option | Type | Default | Description | |--------|------|---------|-------------| | strictSparseArrays | boolean | false | When true, a missing array slot and an explicit undefined at the same index are treated as different | | normalizeBoxedPrimitives | boolean | false | When true, boxed primitives (new String, new Number, new Boolean) are unwrapped to their primitive value before comparison | | allowPrototypeMismatch | boolean | false | When true, objects with different prototype chains are considered equal if their enumerable own properties match |


Known Limitations

Circular reference non-isomorphism: When a cycle is detected, the comparison optimistically returns true to break recursion. As a result, two structurally different cycles — for example, a self-loop (a.self = a) versus a two-node mutual reference (a.next = b; b.next = a) — may be reported as equal when they are not.

WeakMap / WeakSet / Promise: These types are always false because their contents cannot be enumerated.


Tests

This library uses Vitest for testing.

yarn test
# or
npm run test

Test coverage includes:

  • Primitives and edge cases (NaN, BigInt, Symbol)
  • Objects (key order, symbol-keyed properties, prototype mismatch)
  • Arrays (nested, sparse)
  • Date, RegExp, Error (with cause and custom properties)
  • Map and Set (insertion-order independence, object keys/values)
  • Typed arrays, ArrayBuffer, DataView
  • Circular references
  • Boxed primitives (with and without normalization)
  • Functions, WeakMap, WeakSet, Promise

Refer to the test/ folder for the full test suite.


License

MIT © is-deep-equal