shallow-equal review
shallow-equal 3.1.0 compares one level of an array or plain record and returns a boolean. We measured a 0.7 KB minified, 0.4 KB gzipped browser bundle, with no runtime dependencies. The array function checks length and each indexed value with ===. The object function checks enumerable own string-keyed properties, their count and their values, also with ===. A generic function chooses between those two paths after Array.isArray checks. Version 3.1.0 corrected the package's modern `.mjs` reference; the comparison rules themselves stayed small and explicit.
shallow-equal 3.1.0 added 0.4 KB gzipped and no dependencies in our sandbox, making it a cheap fit for immutable data whose top-level references carry meaning. Skip it for nested structures or built-in collection types, and note that the code has not moved since February 2023.
We installed it
| Install | ✓ · 0.6s | 1 package on disk · 1 MB |
| Import | ✓ | ESM import works · require() works · CommonJS package |
| Browser | 0.4 KB | gzipped (0.7 KB minified), bundled with esbuild |
| Types | ✓ | TypeScript types bundled |
| Known vulns | 0 | 0 critical · 0 high · 0 moderate · 0 low (npm audit) |
Answers from our run
Does shallow-equal install cleanly?
Yes. In a fresh container with an empty cache, npm install shallow-equal finished in 0.6s, leaving 1 package and 1 MB on disk. npm audit reported no known vulnerabilities.
How much does shallow-equal add to a browser bundle?
0.4 KB gzipped (0.7 KB minified) when the whole package is bundled for the browser with esbuild. Importing only part of it is usually smaller.
Does shallow-equal work with both ESM and CommonJS?
Yes. Both import 'shallow-equal' and require('shallow-equal') worked in Node 22 in our run. The package is published as CommonJS.
Does shallow-equal include TypeScript types?
Yes, type declarations ship inside the package, so no @types install is needed.
shallow-equal or fast-shallow-equal: which should you use?
fast-shallow-equal: Choose it when benchmarked shallow object comparison is the only operation you need. shallow-equal 3.1.0 added 0.4 KB gzipped and no dependencies in our sandbox, making it a cheap fit for immutable data whose top-level references carry meaning.
When should you not use shallow-equal?
You need nested structural equality; separately created child objects fail because values are compared with ===
Use it if
- You use immutable updates and need to detect whether top-level array items or object fields kept the same references
- You want named array and object comparators without pulling a deep-equality implementation into a browser route
- Your inputs may be null or undefined and you want identical nullish values handled by the same API
- You need bundled TypeScript declarations and compatibility with both require() and ESM import
- You need nested structural equality; separately created child objects fail because values are compared with ===
- You compare Date, Map, Set, RegExp or typed-array contents; their meaningful state is not covered by Object.keys on the wrapper object
- Your number semantics must treat NaN as equal or distinguish 0 from -0; === does the opposite in both cases
- You compare symbol or non-enumerable properties; shallowEqualObjects only visits Object.keys results
- You want active feature development or quick compatibility fixes; version 3.1.0 and the last repository push both date to February 2023
Setup reality
In our install, shallow-equal 3.1.0 finished in 0.6 seconds and left 1 package using 1 MB. npm audit found 0 known vulnerabilities. The package has 0 direct dependencies, 0 peers, an MIT license and 48 KB unpacked. It is CommonJS without an exports map, while require() and ESM import both worked. TypeScript declarations are included. Our browser import built to 0.7 KB minified and 0.4 KB gzipped.
Version 3.1.0 needs no credentials, config file or initialization. Import shallowEqualArrays or shallowEqualObjects when the input kind is already known. The generic shallowEqual first checks both values with Array.isArray, returns false for an array versus an object, and otherwise delegates. The README explicitly calls out that extra runtime type check as the cost of the generic form.
Comparison stops after one level. Two nested objects pass only when both parents point at the same nested reference. Object order does not matter, since the code compares key counts and then checks each key from the first input. Symbols and inherited fields are skipped. An own field set to undefined remains different from a missing field because the second object must pass hasOwnProperty for every key.
Every value comparison uses === instead of Object.is. That makes [NaN] unequal to [NaN], while [0] equals [-0]. Sparse array holes read as undefined, so a single hole compares equal to an explicit undefined element at the same index. Date and Map instances can compare equal despite different contents because they usually expose no enumerable own string keys. Use a type-aware or deep comparator for those values.
Patterns
Compare array entries at one level compare-arrays
import { shallowEqualArrays } from 'shallow-equal'
shallowEqualArrays([1, 2], [1, 2]) // true
shallowEqualArrays([{ id: 1 }], [{ id: 1 }]) // falseEvery indexed value is checked with ===. Equal-looking objects fail when they are separate references.
Compare enumerable record fields compare-objects
import { shallowEqualObjects } from 'shallow-equal'
shallowEqualObjects(
{ page: 2, sort: 'newest' },
{ sort: 'newest', page: 2 },
) // trueProperty order is irrelevant. Only enumerable own string keys returned by Object.keys participate.
Dispatch between array and object comparison dispatch-by-input-kind
import { shallowEqual } from 'shallow-equal'
shallowEqual(['a'], ['a']) // true
shallowEqual({ 0: 'a' }, ['a']) // falseshallowEqual runs Array.isArray on both values before delegating. Prefer the specific export when callers already know the input kind.
Compare records that share a nested reference preserve-child-reference
const filters = { state: 'open' }
const previous = { page: 1, filters }
const unchanged = { page: 1, filters }
const rebuilt = { page: 1, filters: { state: 'open' } }
shallowEqualObjects(previous, unchanged) // true
shallowEqualObjects(previous, rebuilt) // falseImmutable updates fit this contract when unchanged children retain their references and changed children receive new ones.
Compare optional values handle-null-values
shallowEqualObjects(undefined, undefined) // true
shallowEqualObjects(null, null) // true
shallowEqualObjects(null, {}) // false
shallowEqualArrays(undefined, []) // falseThe declarations allow null and undefined. Only the same nullish value succeeds through the initial === check.
Keep missing and undefined fields distinct distinguish-missing-key
shallowEqualObjects(
{ value: undefined },
{ other: undefined },
) // false
shallowEqualObjects(
{ value: undefined },
{ value: undefined },
) // trueThe comparator checks key counts and hasOwnProperty, so an absent key cannot masquerade as an own key holding undefined.
Test strict-equality number behavior account-for-number-edges
shallowEqualArrays([NaN], [NaN]) // false
shallowEqualArrays([0], [-0]) // trueStrict === makes NaN unequal to itself and treats signed zero values as equal. Object.is uses the reverse behavior for both cases.
Normalize holes when array shape matters avoid-sparse-array-surprise
const sparse = new Array(1)
const explicit = [undefined]
shallowEqualArrays(sparse, explicit) // true
const normalized = Array.from(sparse, (value, index) =>
index in sparse ? value : '<hole>'
)The loop reads indexed values and never checks property presence. A hole therefore matches an explicit undefined element.
Use a deliberate React memo comparator compare-react-props
const Row = React.memo(
function Row({ label }) {
return <span>{label}</span>
},
(previous, next) => shallowEqualObjects(previous, next),
)React.memo already uses Object.is for its default shallow prop check. Supplying this comparator changes the NaN and signed-zero cases.
Use type-specific checks for Date and Map avoid-special-objects
shallowEqualObjects(new Date(0), new Date(86400000)) // true
shallowEqualObjects(new Map([['a', 1]]), new Map([['b', 2]])) // true
const sameDate = first.getTime() === second.getTime()Date timestamps and Map entries are not enumerable own string fields, so Object.keys sees both examples as empty objects.
Cache a result by shallow argument equality build-one-entry-cache
let previousArgs
let previousResult
function sum(...numbers) {
if (previousArgs && shallowEqualArrays(previousArgs, numbers)) {
return previousResult
}
previousArgs = numbers
previousResult = numbers.reduce((total, value) => total + value, 0)
return previousResult
}Cache correctness depends on inputs staying immutable. Mutating a retained object can hide a change because the nested reference remains identical.
Pin shallow behavior in a unit test test-reference-contract
const profile = { role: 'editor' }
expect(shallowEqualObjects(
{ profile, active: true },
{ profile, active: true },
)).toBe(true)
expect(shallowEqualObjects(
{ profile },
{ profile: { role: 'editor' } },
)).toBe(false)Test both retained and recreated child references. That catches a refactor that silently changes the application's immutability assumptions.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| fast-shallow-equal | npm | Choose it when benchmarked shallow object comparison is the only operation you need |
| shallowequal | npm | Choose it when an established single shallow comparator better matches an older dependency's API |
| fast-deep-equal | npm | Choose it when nested object contents must compare structurally and its supported built-in types cover your data |
| dequal | npm | Choose it for a compact deep comparator with documented support for Map, Set, Date and typed arrays |
More utils guides
lru-cache · ajv · type-fest · p-limit · find-up · js-yaml · the whole shelf →
How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.

