shallow-equal
shallow-equal is a dependency-free TypeScript utility with three named functions: shallowEqualArrays compares array length and each element with ===, shallowEqualObjects compares own enumerable string keys and each value with ===, and shallowEqual chooses between those paths after checking whether both inputs are arrays. It is useful for memoization and change detection when nested values are intentionally treated by reference, but it is not a structural or deep equality check.
A clear and very small helper when your contract is exactly shallow === comparison over arrays or plain records. Walk away when values include special objects, symbols, NaN, mutable nested data, or any requirement that sounds like structural equality.
Use it if
- You need a tiny, typed shallow comparator for arrays or plain record-like objects and want no runtime dependencies
- Your data follows immutable-update rules, so a changed nested value also receives a new reference
- You want separate specialized array and object functions to avoid the generic comparator's runtime Array.isArray checks
- You need both ESM-aware and CommonJS bundler entry points from the same package
- You need deep equality: two separately created nested objects compare false even when every nested property has the same content, exactly as the README examples show
- You compare Date, RegExp, Map, Set, typed-array metadata, or other special objects: the object function only uses Object.keys, so two different Dates or Maps with no enumerable keys can compare true
- You expect Object.is semantics: version 3.1.0 uses ===, which means NaN differs from NaN while positive zero and negative zero compare equal
- Symbol keys, non-enumerable properties, inherited properties, prototypes, and property descriptors matter: Object.keys ignores all of them and the implementation does not compare prototypes
- You need active evolution or current compatibility work: the latest release and last repository push were both in February 2023, and the open repository has no documented release cadence
Setup reality
Install with npm install shallow-equal. Version 3.1.0 has no runtime dependencies, native code, peer dependencies, configuration, credentials, or environment variables. It publishes a CommonJS main file, a modern .mjs module hint, and bundled TypeScript declarations. Import its named exports: shallowEqual, shallowEqualArrays, or shallowEqualObjects. There is no default export from the package index. The specialized functions also make the intended input shape clearer and avoid the generic function's two Array.isArray calls. The important setup work is agreeing on data semantics. Every nested array, object, and function is compared by reference with ===, so in-place mutation can be missed when the top-level references stay the same. Conversely, recreating an equal nested object makes the comparison fail. Object comparison uses own enumerable string keys only and deliberately ignores order; array comparison is positional and treats a sparse hole like an explicit undefined at that index because direct indexing returns undefined. null and undefined compare equal only to themselves, while one falsy input against a different value returns false. The library does not support a custom comparator, ignored-key list, cycle traversal, or special handling for Dates, Maps, Sets, React elements, NaN, and signed zero. TypeScript types narrow inputs to records, arrays, null, or undefined, but they cannot ensure that a record contains only plain data. Add focused tests around your actual values before putting this comparator on a caching, rendering, or persistence boundary.
Patterns
Compare array elements shallowlycompare-arrays
import { shallowEqualArrays } from 'shallow-equal'
shallowEqualArrays([1, 2, 3], [1, 2, 3]) // true
shallowEqualArrays([{ id: 1 }], [{ id: 1 }]) // falseElements use ===. Separately allocated objects are different even when their properties match.
Compare own enumerable object fieldscompare-objects
import { shallowEqualObjects } from 'shallow-equal'
shallowEqualObjects(
{ page: 2, query: 'books' },
{ query: 'books', page: 2 },
) // trueObject key order does not matter. Symbol, inherited, and non-enumerable properties are not part of the comparison.
Choose array or object comparison at runtimecompare-generic
import { shallowEqual } from 'shallow-equal'
shallowEqual(['a', 'b'], ['a', 'b']) // true
shallowEqual({ 0: 'a', 1: 'b' }, ['a', 'b']) // falseThe generic function first checks both inputs with Array.isArray. Use a specialized function when the collection kind is already known.
Compare immutable records with shared nested valuesreuse-nested-references
const filters = { status: 'open' }
const before = { page: 1, filters }
const same = { page: 1, filters }
const changed = { page: 1, filters: { status: 'open' } }
shallowEqualObjects(before, same) // true
shallowEqualObjects(before, changed) // falseThis works well with immutable updates: preserve references for unchanged nested values and allocate new ones for changes.
Cache one result by a shallow argument listmemoize-one-entry
import { shallowEqualArrays } from 'shallow-equal'
let previousArgs
let previousResult
function total(...numbers) {
if (previousArgs && shallowEqualArrays(previousArgs, numbers)) return previousResult
previousArgs = numbers
previousResult = numbers.reduce((sum, value) => sum + value, 0)
return previousResult
}Copy or replace an argument array before later mutation. Holding a mutable array reference can make a cache report a false match.
Use an explicit React memo comparatorcompare-react-props
import { memo } from 'react'
import { shallowEqualObjects } from 'shallow-equal'
const Row = memo(
function Row(props) {
return <span>{props.label}</span>
},
(previous, next) => shallowEqualObjects(previous, next),
)React.memo already shallow-compares props with Object.is by default. This changes edge behavior for NaN and signed zero, so add it only for a deliberate reason.
Compare optional recordshandle-nullish
shallowEqualObjects(undefined, undefined) // true
shallowEqualObjects(null, null) // true
shallowEqualObjects(null, {}) // false
shallowEqualObjects(undefined, null) // falseThe TypeScript signature accepts null and undefined. Only identical nullish values pass the initial === check.
Account for strict-equality number edge casesrecognize-nan-semantics
shallowEqualArrays([NaN], [NaN]) // false
shallowEqualArrays([0], [-0]) // true
// Normalize first if your domain needs different semantics.
const normalize = (value) => Number.isNaN(value) ? 'NaN' : valueThe implementation uses === rather than Object.is. This differs from React's default prop comparator and from some deep-equality libraries.
Do not compare Dates or Maps as plain objectsavoid-special-objects
shallowEqualObjects(new Date(0), new Date(86400000)) // true
shallowEqualObjects(new Map([['a', 1]]), new Map([['b', 2]])) // true
const sameInstant = firstDate.getTime() === secondDate.getTime()Date and Map contents are not enumerable own string keys. Use type-specific comparison or a deep comparator that documents support for them.
Distinguish a missing key from undefineddetect-key-presence
shallowEqualObjects({ value: undefined }, { other: undefined }) // false
shallowEqualObjects({ value: undefined }, { value: undefined }) // trueThe function checks both equal values and hasOwnProperty on the second object, so a different key with the same undefined value does not pass.
Normalize sparse arrays when holes matterunderstand-sparse-arrays
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 comparator checks length and indexed values, not property presence, so an array hole and explicit undefined are treated as equal.
Lock the intended reference contract in teststest-shallow-contract
const shared = { role: 'admin' }
expect(shallowEqualObjects(
{ user: shared, active: true },
{ user: shared, active: true },
)).toBe(true)
expect(shallowEqualObjects(
{ user: shared },
{ user: { role: 'admin' } },
)).toBe(false)Test both shared and recreated nested references. Otherwise a later refactor from immutable to in-place updates can invalidate the comparison assumption.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| fast-shallow-equal | npm | Use when you want another small shallow object comparator and its narrower API matches your record-only hot path |
| dequal | npm | Use when nested structure and built-in values such as Date, RegExp, Map, Set, and typed arrays need deep comparison |
| fast-deep-equal | npm | Use when the real requirement is fast deep equality, with a React-specific build available for React element comparisons |