typescript/object/deep-equal@1 — Compare two values by the data they carry rather than by reference, walking into arrays, objects, Map, Set, typed arrays, Date, RegExp and Error, terminating on graphs that return to themselves, and never writing to either argument.

49 named edge cases, settled and frozen. TypeScript source copied into your project: one file, 13 327 bytes, no dependencies.

[toopo](../../../)

- [How we verify](../../../method/)

`typescript/object/deep-equal@1`

# deep-equal

Compare two values by the data they carry rather than by reference, walking into arrays, objects, Map, Set, typed arrays, Date, RegExp and Error, terminating on graphs that return to themselves, and never writing to either argument.

Install

```
npx toopo add object/deep-equal
```

Signature

```
type DeepEqual = (left: unknown, right: unknown) => boolean
```

**13 327** bytes, one file

**0** imports

**49** settled cases

## What it does

Answers whether two JavaScript values carry the same data. `===` compares references, and the spelling a caller reaches for instead - comparing two JSON.stringify results - is wrong on seven of nine ordinary pairs: it answers false for {a:1,b:2} against {b:2,a:1}, true for a populated Map against an empty one, true for a Date against its own ISO string, true for -0 against 0, and it throws on a cyclic value and on a BigInt. The cause is one line of the language: Object.keys of a Set is the empty array, so a collection carries its contents where no property walk reaches. Measured on fast-deep-equal 3.1.3, deep-equal 2.2.3, dequal 2.0.3, lodash 4.18.1 and the runtime's own util.isDeepStrictEqual, eleven of twenty-eight ordinary pairs are answered differently by at least two of them - and two of those five answer true for new Set(\[1\]) against new Set(\[2\]) and for a populated Map against an empty one, because they walk own properties and a Set has none. The entry point their README sends a caller to for collections answers false for a Set of objects against its own structured clone. This contract settles all of it, and settles two things nothing else does: the comparison does not consume the call stack in proportion to the depth of ordinary structure, and a candidate match inside a collection that fails leaves nothing behind it.

ECMAScript compares two values structurally nowhere: Object.is and === answer about references, and JSON.stringify is not a comparison. Read on 2026-08-23, three stage 1 proposals touch this and none would answer it. Composites, last moved 2026-08-18, gives value equality to purpose-built frozen string-keyed structures compared with === after interning - it is shallow, cannot hold a cycle and cannot be asked about two objects that already exist. Array Equality, last moved 2021-04-22, is scoped to arrays. Comparisons, last moved 2026-06-11, is about assertion functions and its open question is whether it should cover rich equality comparisons at all; that is the proposal this contract is re-examined against. The runtime's util.isDeepStrictEqual is a host's assertion helper, absent from the browser, specified nowhere, and the first of six to fall over on depth.

### What it is for, and what it is not

The values structuredClone carries: primitives, plain objects, arrays, Map, Set, Date, RegExp, Error, typed arrays, ArrayBuffer, DataView, boxed primitives, and any graph made of them, including one that returns to itself. It is not a schema validator, not a similarity score and not an assertion - it answers and never throws. It does not read into a Promise, a WeakMap, a WeakSet, a WeakRef or a function, because nothing about their contents is observable; two distinct such values are answered false rather than guessed at. It compares prototypes, so an instance of a class and a plain object carrying the same fields are two different things, and a caller who wants them equal wants a different function.

## Try it on your own input

This calls deepEqual on whatever you type. What you type into a field is the value, character for character, and the form opens on not-a-number-equals-itself so there is a call that works to edit. left and right are written as a literal instead, exactly the way the cases above are written, because what it takes is any value at all, which is what this contract compares. What comes back is what the function answered, under the call it was made from — invisible characters are named there, so two inputs that look alike on screen do not print alike. The settled answer is on the case's own line below, and is deliberately not repeated here.

The JavaScript this runs is object/deep-equal's own reference.ts with its types stripped. That is neither the file the registry serves nor the file its digest covers: both are TypeScript, and no browser runs TypeScript. It is also the only part of this page that needs JavaScript at all.

## Reference

Everything above answers whether this function does what you need. Everything below is [what it is bound to do](../../../what-a-contract-is/), in full. It is long on purpose, and none of it is folded away: a case a reader cannot find with their browser's own search is a case this catalogue did not really publish.

## Signature

You get deepEqual.

```
type DeepEqual = (left: unknown, right: unknown) => boolean
```

## 49 settled cases

Every one of them is named, frozen with the major version, and linkable. This is what the contract decides, one input at a time.

the pairs this contract settles, each answered by the walk and by the transposition of itself, because an order-insensitive matching can answer differently by side.

- [Identity of a primitive 5](#identity-of-a-primitive)
- [The shape of a plain object 6](#the-shape-of-a-plain-object)
- [Arrays and their holes 2](#arrays-and-their-holes)
- [Collections carry what no key walk sees 7](#collections-carry-what-no-key-walk-sees)
- [A value in an internal slot 15](#a-value-in-an-internal-slot)
- [An error is data 4](#an-error-is-data)
- [A graph that returns to itself 5](#a-graph-that-returns-to-itself)
- [What the domain does not carry 3](#what-the-domain-does-not-carry)
- [What a clone does not preserve 2](#what-a-clone-does-not-preserve)

### Identity of a primitive

Where the walk stops before it starts. Every one of these is decided by `Object.is`, which is the language's own rule and the reason `NaN` equals itself here while `-0` does not equal `0`.

`deepEqual(NaN, NaN) → true`

[\#not-a-number-equals-itself](#not-a-number-equals-itself)

`NaN === NaN` is false and `Object.is(NaN, NaN)` is true. A comparison about data takes the second: a caller holding two records that both failed to parse a number is holding the same record twice, not two different ones.

`deepEqual(-0, 0) → false`

[\#negative-zero-is-not-zero](#negative-zero-is-not-zero)

The other half of `Object.is`, and the half the ecosystem splits on: `lodash` and `fast-deep-equal` answer true, `util.isDeepStrictEqual` answers false. A refund that rounded away to nothing keeps the mark saying which way the money went - the same argument `number/round@1` freezes one contract over.

`deepEqual({ z: -0 }, { z: 0 }) → false`

[\#negative-zero-nested-is-still-not-zero](#negative-zero-nested-is-still-not-zero)

The same rule one level down, because a rule that held only at the root would be a property of the entry point rather than of the walk.

`deepEqual({ v: 1n }, { v: 1 }) → false`

[\#a-bigint-is-not-its-number](#a-bigint-is-not-its-number)

`1n == 1` is true and `Object.is(1n, 1)` is false. Two values a caller cannot add together are not the same datum, and the loose comparison is the one nothing here uses.

`deepEqual(new String('a'), 'a') → false`

[\#a-boxed-primitive-is-not-its-primitive](#a-boxed-primitive-is-not-its-primitive)

One is an object with a prototype and own indexed properties, the other is a primitive. `typeof` separates them and so does anything a caller does with them afterwards.

### The shape of a plain object

`deepEqual({ a: 1, b: 2 }, { b: 2, a: 1 }) → true`

[\#the-order-keys-were-written-in-is-not-read](#the-order-keys-were-written-in-is-not-read)

The first thing the spelling a caller reaches for gets wrong: comparing two `JSON.stringify` results answers false here, because a serialisation carries an order the data does not have.

`deepEqual({ a: undefined }, {}) → false`

[\#a-key-holding-undefined-is-not-a-missing-key](#a-key-holding-undefined-is-not-a-missing-key)

The two are different to `Object.keys`, to `in`, and to a caller writing `if ("a" in row)`. The `JSON.stringify` spelling answers true, because `undefined` is dropped on the way out.

`deepEqual({ [#1 = Symbol('object/deep-equal@1 case')]: 1 }, { [#1]: 2 }) → false`

[\#data-under-a-symbol-key-is-data](#data-under-a-symbol-key-is-data)

Measured, four of six shipped implementations answer true here: they walk `Object.keys`, which does not report a symbol. Two objects whose data differs compare equal, and nothing about the call says so.

`deepEqual(<an instance of a class>, { x: 1 }) → false`

[\#a-class-instance-is-not-its-fields](#a-class-instance-is-not-its-fields)

The prototype is part of what the value is: methods, a constructor and whatever else the class carries are all reachable from one and not from the other. Every implementation measured agrees, and `inputDomain` says in as many words that a caller who wants them equal wants a different function.

`deepEqual(Object.assign(Object.create(null), { a: 1 }), { a: 1 }) → false`

[\#a-null-prototype-object-is-not-a-plain-one](#a-null-prototype-object-is-not-a-plain-one)

The same rule at the other end of the prototype chain, and the place the ecosystem parts again: `lodash` answers true, `util.isDeepStrictEqual` answers false. An object built to have no inherited `toString` is not the object that has one.

`deepEqual({ a: 1 }, { a: 1 }) → true`

[\#a-getter-is-read-as-the-value-it-returns](#a-getter-is-read-as-the-value-it-returns)

A comparison reads values, and reading a property is what a caller does too. The alternative - comparing descriptors - would answer about how an object was written rather than about what it carries, and would make a memoised object differ from its own plain copy.

### Arrays and their holes

An array is compared by its own keys, so a hole and a stored `undefined` are different things - which is what `Object.keys` says about them and what nothing that walks values alone can see.

`deepEqual([, 1], [undefined, 1]) → false`

[\#a-hole-is-not-an-undefined](#a-hole-is-not-an-undefined)

`Object.keys([,1])` is `["1"]` and `Object.keys([undefined,1])` is `["0","1"]`, so the two differ in what they hold and not only in what they read as. `map` and `forEach` skip the hole and visit the `undefined`, which is where a caller meets the difference.

`deepEqual([1, 2], [1, 2, ,]) → false`

[\#a-trailing-hole-changes-the-length](#a-trailing-hole-changes-the-length)

Own keys are identical here and the lengths are 2 and 3, so this is the row that says the walk compares an array's length as well as its keys. Without it a table built only from key sets would pass an implementation that dropped the length check.

### Collections carry what no key walk sees

The reason this contract exists. `Object.keys(new Set([1]))` is the empty array, so an implementation that walks own properties sees two empty objects and says they match - measured, two of five shipped implementations answer `true` for `new Set([1])` against `new Set([2])`.

`deepEqual(new Set([1]), new Set([2])) → false`

[\#two-sets-of-different-members-are-different](#two-sets-of-different-members-are-different)

The sharpest instance in the catalogue of a silent wrong answer: measured, `fast-deep-equal@3.1.3` and `dequal/lite@2.0.3` both answer true, because a Set has no own enumerable property and a walk over keys sees two empty objects.

`deepEqual(new Set([1, 2]), new Set([1])) → false`

[\#two-sets-of-different-size-are-different](#two-sets-of-different-size-are-different)

The same two implementations answer true here too, which is what separates this from the row above: it is not that they compare members badly, it is that they do not look at the collection at all.

`deepEqual(new Map([['a', 1]]), new Map([])) → false`

[\#a-populated-map-is-not-an-empty-one](#a-populated-map-is-not-an-empty-one)

The `JSON.stringify` spelling also answers true here, and for the same reason as the two above rather than a different one - a Map serialises as `{}`. Three ways of asking, one blindness.

`deepEqual(new Map([['a', 1]]), new Map([['a', 2]])) → false`

[\#a-map-value-is-compared](#a-map-value-is-compared)

A Map is compared by its entries and not only by its keys, which is the half an implementation comparing sizes and key sets would pass without.

`deepEqual(new Set([1, 2]), new Set([2, 1])) → true`

[\#the-order-members-were-added-in-is-not-read](#the-order-members-were-added-in-is-not-read)

Insertion order is observable on a Set - it decides iteration - and it is not part of what the Set holds. A caller who built the same set of ids from two different queries has one set of ids.

`deepEqual(new Set([{ id: 1 }]), new Set([{ id: 1 }])) → true`

[\#a-set-member-is-compared-by-its-data](#a-set-member-is-compared-by-its-data)

The other direction of the same blindness, and the one the ecosystem's remedy gets wrong: `fast-deep-equal/es6` \- the entry point its README sends a caller to for collections - answers false, because it tests membership with `has`, which is reference identity.

`deepEqual(new Map([[{ k: 1 }, 'v']]), new Map([[{ k: 1 }, 'v']])) → true`

[\#a-map-key-is-compared-by-its-data](#a-map-key-is-compared-by-its-data)

The same for a key rather than a member, which is the harder half: matching entries without an order means a key of one has to be found among the unclaimed keys of the other, and that is the only quadratic path in the contract.

### A value in an internal slot

The same blindness one floor down: a `Date`, a boxed number and a typed array carry their value where no own property is, and a walk that enumerates keys sees nothing to compare.

`deepEqual(new Date(0), new Date(0)) → true`

[\#two-instants-are-one-date](#two-instants-are-one-date)

A Date carries a number of milliseconds in a slot and has no own property at all. Two dates naming one instant are one datum.

`deepEqual(new Date(NaN), new Date(NaN)) → true`

[\#an-invalid-date-equals-an-invalid-date](#an-invalid-date-equals-an-invalid-date)

The slot holds `NaN`, and the rule is the one the first group settles: `Object.is` and not `===`. Measured, three of five shipped implementations answer false here, which is `NaN`'s own trap arriving inside a built-in.

`deepEqual(new Date(0), new Date(NaN)) → false`

[\#an-invalid-date-is-not-an-instant](#an-invalid-date-is-not-an-instant)

The neighbour of the row above, and the reason that row is about `Object.is` rather than about treating every invalid date as equal to everything.

`deepEqual(/ab+c/gi, /ab+c/gi) → true`

[\#a-pattern-is-its-source-and-its-flags](#a-pattern-is-its-source-and-its-flags)

The two things a pattern is. Everything else about a RegExp object is derived from them or is the state of a scan, which the last row of this group settles.

`deepEqual(/a/g, /b/g) → false`

[\#a-pattern-source-is-part-of-it](#a-pattern-source-is-part-of-it)

The half of the row above that says `source` is read.

`deepEqual(/a/g, /a/i) → false`

[\#a-pattern-flag-is-part-of-it](#a-pattern-flag-is-part-of-it)

The other half, and not the same claim: an implementation comparing only `source` passes the row above and fails here.

`deepEqual(/a/g, /a/g) → true`

[\#where-a-pattern-stopped-is-not-part-of-it](#where-a-pattern-stopped-is-not-part-of-it)

`util.isDeepStrictEqual` reads `lastIndex` and answers false; `lodash`, `dequal` and `fast-deep-equal` ignore it. **This contract excludes it and the argument is not a show of hands.** Measured: `structuredClone(/a/g)` with `lastIndex` at 3 returns a pattern whose `lastIndex` is 0, with `source` and `flags` identical, and the same for a sticky pattern. The declared domain is the values `structuredClone` carries and the admission oracle is that a value equals its own clone, so reading `lastIndex` would make this contract fail the oracle it was admitted on. `util.isDeepStrictEqual` is bound by no such domain; this contract is.

`deepEqual(new Number(7), new Number(8)) → false`

[\#a-boxed-number-carries-a-number](#a-boxed-number-carries-a-number)

**A row the author of this contract got wrong.** A boxed number has no own property, so a walk that enumerates keys sees two empty objects and answers true - which is what this contract's own walk did, silently, until the dispatch was measured. `lodash` and `util.isDeepStrictEqual` were right about it all along.

`deepEqual(new Boolean(true), new Boolean(false)) → false`

[\#a-boxed-boolean-carries-a-boolean](#a-boxed-boolean-carries-a-boolean)

The same slot blindness on a second kind, kept as its own row because a dispatch repaired for `Number` alone passes the row above and fails here - which is how the repair was checked.

`deepEqual(Object(1n), Object(2n)) → false`

[\#a-boxed-bigint-carries-a-bigint](#a-boxed-bigint-carries-a-bigint)

The third kind, and the one that is easiest to leave out because `Object(1n)` is not a spelling anybody writes on purpose - which is exactly why a caller meets it through a library rather than through their own code.

`deepEqual(new Number(7), Object.assign(new Number(7), { tag: 1 })) → false`

[\#a-slot-does-not-hide-an-own-property](#a-slot-does-not-hide-an-own-property)

The slots agree and the own keys do not. Measured, `lodash` answers true here - it reads the slot and stops - so this row is one of the two places this contract is stricter than the most-used implementation rather than looser.

`deepEqual(new Uint8Array([1, 2]), new Uint8Array([1, 2])) → true`

[\#two-typed-arrays-of-one-kind-are-their-elements](#two-typed-arrays-of-one-kind-are-their-elements)

A typed array has indexed own properties, so a key walk finds something - which is why this row passes almost everywhere and the next two do not.

`deepEqual(new Uint8Array([1, 0]), new Uint16Array([1])) → false`

[\#a-typed-array-kind-is-part-of-it](#a-typed-array-kind-is-part-of-it)

Same bytes, different kinds, different lengths, and a caller cannot use one where the other is expected. The prototypes differ, which is what the walk reads.

`deepEqual(new Float64Array([NaN]), new Float64Array([NaN])) → true`

[\#not-a-number-inside-a-float-array-equals-itself](#not-a-number-inside-a-float-array-equals-itself)

The row that caught three of the five shipped implementations against the oracle: they compare elements with `===`, so a float array holding `NaN` is not equal to its own structured clone. `Object.is` per element is the rule, and it is the first group's rule again rather than a new one.

`deepEqual(new Float64Array([-0]), new Float64Array([0])) → false`

[\#negative-zero-inside-a-float-array-is-not-zero](#negative-zero-inside-a-float-array-is-not-zero)

The other half of `Object.is` inside a typed array, and the half an implementation comparing bytes would get right by accident - which is a different implementation from one that got the row above right.

### An error is data

`deepEqual(new Error('x'), new Error('x')) → true`

[\#two-errors-of-one-message-are-one-error](#two-errors-of-one-message-are-one-error)

An error caught and stored is a value like any other. `stack` is not read, because it names where the error was constructed rather than what it says, and two errors built at two call sites for one reason are one reason.

`deepEqual(new Error('a'), new Error('b')) → false`

[\#a-message-is-part-of-an-error](#a-message-is-part-of-an-error)

Measured, `dequal` answers true here - it walks own enumerable properties and `message` is not one of them. The same blindness as the collections, on the type a caller is most likely to be comparing when something has gone wrong.

`deepEqual(new Error('a', { cause: 1 }), new Error('a', { cause: 2 })) → false`

[\#a-cause-is-part-of-an-error](#a-cause-is-part-of-an-error)

`cause` is where the original failure is kept, so an implementation reading `name` and `message` alone answers true for two errors that happened for different reasons. It is compared as data rather than by identity, so a cause that is itself an object is walked.

`deepEqual(new TypeError('x'), new Error('x')) → false`

[\#an-error-kind-is-part-of-it](#an-error-kind-is-part-of-it)

The prototype rule of the second group, arriving where a caller most often branches on it: code that asks `err instanceof TypeError` is asking about exactly this difference.

### A graph that returns to itself

Terminating is half of it. The other half is that a comparison which remembers what it has already assumed must forget an assumption it made speculatively, and the last two rows are the ones the author got wrong.

`deepEqual(#1 = { name: 'root', self: #1 }, #2 = { name: 'root', self: #2 }) → true`

[\#a-cycle-terminates](#a-cycle-terminates)

Measured, three of six shipped implementations do not answer this at all - they exhaust the call stack. Answering is the claim; answering `true` is the second half, and it is the coinductive reading: two graphs that unfold to the same infinite tree are the same data.

`deepEqual(#1 = new Set([#1]), #2 = new Set([#2])) → true`

[\#a-set-containing-itself-terminates](#a-set-containing-itself-terminates)

The same claim through a collection rather than through a property, and it is not the same test: matching members recurses, so an implementation whose cycle detection lives only in the property walk answers the row above and never returns from this one.

`deepEqual(#1 = { name: 'root', self: #1 }, { name: 'root', self: { name: 'root', self: null } }) → false`

[\#a-cycle-is-not-the-same-as-its-unrolling](#a-cycle-is-not-the-same-as-its-unrolling)

The row that stops the one above from being satisfied by an implementation that answers `true` whenever it has seen a pair before. A cycle and a finite unrolling of it part at the first place the unrolling ends.

`deepEqual({ s: new Set([#1 = { v: 1 }, { v: 2 }]), also: [#1] }, { s: new Set([{ v: 2 }, { v: 1 }]), also: [{ v: 2 }] }) → false`

[\#a-failed-candidate-leaves-nothing-behind](#a-failed-candidate-leaves-nothing-behind)

**The row the author of this contract got wrong, in the hour after writing down the danger of cycle detection.** The two Sets match either way round; `also` holds `{v:1}` against `{v:2}`. An implementation that memoises the pairs a failed candidate tried answers `true`, because the failed attempt left `({v:1}, {v:2})` marked as assumed equal. All four shipped implementations answer `false`, so this row founds no admission - what it founds is the clause, and the evidence for the clause is that the fault was committed here.

Since this was published: The two rows are correct rows - `false` is the answer, and every shipped implementation measured gives it - and they do not separate the defect their rationale names. The clause the contract states about speculation stands; what does not stand is that these two rows witness it. **Nothing in this repository could have said so**: a rationale is prose beside a correct answer, and only injecting the defect and watching nothing redden found it.

Measured at `3ec621c` by injecting exactly that defect into this contract's own reference - the mutant `object-deep-equal · DE-01`, which replaces the line taking a pair back off the path with a no-op - and running it against the sound version over the four forms of the witness. The memoising walk answers `false` on this row and on `and-answers-the-same-either-way-round`, exactly as the sound one does. It answers `true` only when the keys are transposed **and** the right-hand `also` holds the very Set member the failed candidate tried; the witness these rows are built from holds a fresh object there, and the path is keyed by identity, so the pair the failed candidate left behind is never asked for again. The reading is of this reference and of no other: an implementation that memoises differently may well answer `true` here.

`deepEqual({ also: [#1 = { v: 1 }], s: new Set([#1, { v: 2 }]) }, { also: [{ v: 2 }], s: new Set([{ v: 2 }, { v: 1 }]) }) → false`

[\#and-answers-the-same-either-way-round](#and-answers-the-same-either-way-round)

The same two values with their two keys declared in the other order, and nothing else changed. The walk that got the row above wrong answered `false` here, so a table carrying only one of the two would have been green on half the defect - which is what makes this a row rather than a note.

Since this was published: The sentence is true of this row and false of the pair. `so a table carrying only one of the two would have been green on half the defect` reads as though the two rows split the defect between them; measured, both are green on all of it. What the pair still does is what a transposition is for - it holds the answer steady under a reordering nothing should read - and that is a smaller claim than the one published.

The same reading at `3ec621c`, and this row is wrong about its neighbour rather than about itself: the memoising walk really does answer `false` here, and it also answers `false` on the row above. Transposing the keys is one of the two things the separating form needs and this row carries that one; what it does not carry is the right-hand `also` holding the very Set member the failed candidate tried.

### What the domain does not carry

Values `structuredClone` refuses. The contract answers `false` rather than guessing or throwing, because a silent `true` on two different functions is the defect this whole catalogue is written against.

`deepEqual(<a value whose contents cannot be read>, <a value whose contents cannot be read>) → false`

[\#two-promises-are-not-compared](#two-promises-are-not-compared)

Nothing about a promise's contents is readable synchronously, so there is nothing to compare. `false` rather than a throw, because the caller asked a question with an answer: these are not the same value.

`deepEqual(<a value whose contents cannot be read>, <a value whose contents cannot be read>) → false`

[\#two-weak-collections-are-not-compared](#two-weak-collections-are-not-compared)

A WeakMap cannot be enumerated by design. Answering `true` for two of them - which the author's own walk did, because they have no own property - is the silent `true` this contract exists to refuse, arriving on a type nobody thinks to test.

`deepEqual(<a function, served as a file>, <a function, served as a file>) → false`

[\#two-functions-are-not-compared](#two-functions-are-not-compared)

Two functions are equal when they are the same reference and not otherwise: source text is not behaviour, and a closure carries what no reading of it shows. The author's walk answered `true` for these two as well.

### What a clone does not preserve

`p1-reflexive-under-a-copy` is the oracle this contract was admitted on, and it is not unconditional. `structuredClone` returns a plain object for a class instance and for a null-prototype object, so a value of either shape is not equal to its own clone - measured, and `util.isDeepStrictEqual` answers the same. The property's alphabet excludes both, and these rows are where they are settled instead.

`deepEqual(<an instance of a class>, { x: 2 }) → false`

[\#a-class-instance-is-not-its-own-clone](#a-class-instance-is-not-its-own-clone)

`structuredClone` returns a plain object for a class instance - measured, the prototype goes from the class to `Object` \- so the clone is not the same kind of thing. This row is what `p1-reflexive-under-a-copy` excludes rather than what it asserts, and it was found by fast-check on the first hundred draws while the property was being calibrated.

`deepEqual(Object.assign(Object.create(null), { b: 2 }), { b: 2 }) → false`

[\#a-null-prototype-object-is-not-its-own-clone](#a-null-prototype-object-is-not-its-own-clone)

The same flattening at the other end: a null-prototype object clones to a plain one. `util.isDeepStrictEqual` answers `false` here too and `lodash` answers `true`, so the ecosystem is split on the row that decides how the oracle has to be stated.

## Properties

3 of these 4 are checked on 1 000 generated cases per run, re-seeded each time. The other one is not applicable to this contract, and says why below.

- `never mutates its arguments — checked`

  Both arguments are arbitrary object graphs and the walk has to remember where it has been. Witnessed by an implementation that marks each visited object with a temporary property and removes it on the way out, which is a real technique and leaves a marker on every object it failed to unwind - the shape a walk reaches the first time somebody makes cycle detection "cheaper". The property walks both arguments before and after the call and compares the own keys of every reachable object, so a marker left anywhere in either graph reddens it.
- `deterministic — checked`

  Violable in practice, and witnessed twice over. A cache keyed on one argument answers a second call from the first, and - measured on this contract's own walk while it was being written - a comparison that memoises the pairs a failed candidate tried answers differently depending on the order the keys of an object were declared in, which is the same defect arriving through the data instead of through a cache. This property is ordered under `no ambient input` rather than independent of it: every mutant measured to redden it reddens that one too, and the memoise-last mutant reddens that one and not this.
- `no ambient input — checked`

  Violable in practice: this contract reads two values and answers a boolean, so the call history is the only ambient input it can plausibly acquire. Witnessed by an implementation holding the last pair it was asked and its verdict, and serving that verdict whenever both arguments match by reference - which answers correctly until one of the two graphs is mutated between calls, and then answers the previous question. The property interleaves a probe with an arbitrary history and requires the probe to answer identically either way.
- `no ambient output — not applicable`

  Not reachable by a property - a test cannot observe a write that happened before it ran, and a correct memoising cache is indistinguishable from a defect by behaviour alone.

## Benchmark profiles

The shapes of input an implementation is timed on. No figures yet: there is no reference machine, and a number produced on a developer laptop would be a number with nothing behind it.

- `two-renders-of-one-view — stops-early`

  Shallow objects of a handful of keys that differ in the first one looked at. The dominant shape in memoisation, and the path a caller takes on most calls.
- `a-row-that-changed-at-the-end — stops-late`

  Records agreeing on everything but their last field, so the walk pays the whole traversal and answers false. Timed apart from the profile above because an implementation optimised for the first difference measures nothing here.
- `two-copies-of-one-tree — walks-everything`

  Structurally identical nested objects, walked to the last leaf and answered true. The worst case of the ordinary path and the one that decides whether this can be called in a render.
- `members-without-an-order — walks-everything`

  Sets and Maps holding objects, where a member of one has to be matched against the unclaimed members of the other. The only quadratic path in the contract, and the one the declared collection bound comes from.

## How this contract measured

2 batteries break this implementation on purpose, 26 times, and require the contract's own suite to notice each time. 24 of those defects were caught.

2 cells survived, and what did is here rather than on a page of its own, because what a suite did not catch is a fact about this function. Each carries the battery's own account of why, in the instrument's words and not this page's.

### 2 — its witness is frozen out

the edit is a real defect and an input tells it from the correct version - measured, not assumed. The row that would carry that input cannot be added: the contract owning it is published, and its cases are inside the digest a lockfile holds. It is the one kind here that nothing closes short of a second major.

`object-deep-equal-spec · DS-05` on S/as-committed

adds a fourth class to the profile vocabulary that no profile carries, so the registry serves a word a reader is handed and nothing measures. **It survives, and the guard written for it is why**: `every-class-the-vocabulary-declares-is-sampled` re-declares the union by hand - three strings in an array beside the type - so a fourth member of the type leaves its list at three and it passes. Its own comment claims \*both directions\*, and the second one is dead: a profile declaring a class outside the union does not compile, and a class inside it that no profile carries reddens nothing. Totality would need the union derived from a runtime list rather than transcribed from the type, and `profiles.test.ts` is one of the seven files inside this contract digest. ADR-0161

`object-deep-equal · DE-01` on E/as-committed

memoises the pairs it has compared instead of keeping them on a path, so a candidate that failed inside a Set leaves the pair it tried marked as equal. This is the fault this file was written with, and the answer it produces depends on the order the keys of an object were declared in. **The two rows written to witness it do not separate it**, and only the replay said so: measured over the four forms, `false` against `false` as the witness is published in either key order, and `false` against `true` only when the keys are transposed \*and\* the right-hand `also` holds the very Set member the failed candidate tried. The published witness holds a fresh object there, and the path is keyed by identity - so nothing is ever found again. The row that would witness it is a row of `caseTables`, which is inside the digest a lockfile holds, so it cannot be added

## What you can check yourself

This definition is frozen. Its canonical text hashes to cf0ee281d97aa77d85623e649232e6d7a04d72d3bc607fa27172636a2ba53843, and the 7 files of its test harness are listed inside it with their own hashes — so a copy of the harness can be checked against this definition before it is trusted, then run against any implementation, without taking our word for any of it.

Written for node, browser, bun.

[object · 1 contract](../../../typescript/object/)

- deep-equal

Domains

- [number](../../../typescript/number/)
- [date](../../../typescript/date/)
- [array](../../../typescript/array/)
- [string](../../../typescript/string/)
- [object](../../../typescript/object/)

On this page

- [What it does](#what-it-does)
- [Try it on your own input](#try-it)
- [Reference](#reference)
- [Signature](#signature)
- [49 settled cases](#settled-cases)
- [Properties](#properties)
- [Benchmark profiles](#profiles)
- [How this contract measured](#how-this-measured)
- [What you can check yourself](#checking)
