Skip to content

Latest commit

Β 

History

History
95 lines (77 loc) Β· 5.14 KB

File metadata and controls

95 lines (77 loc) Β· 5.14 KB

@ver0/deep-equal

Deep equality comparison library. Correctness and comprehensive type support over raw performance. Single export: isEqual(a, b).

Commands

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

Architecture

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.

Code Conventions

  • 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 under isolatedDeclarations
  • ESM only β€” "type": "module", .js extensions in imports
  • Vite+ toolchain β€” vp drives vitest, oxlint and oxfmt; everything is configured in vite.config.ts
  • @ver0/oxlint-config β€” javascript, typescript + typescriptUnsafe, node and vitest presets. The unsafe preset disables the no-unsafe-* family (intentional, the core function uses any)
  • Conventional commits β€” feat, fix, perf, refactor, test, chore, docs
  • Semantic release β€” automated versioning, do not manually bump versions

Implementation Patterns

  • Reverse iteration in hot loops: for (let i = length; i-- !== 0;)
  • Self-comparison for NaN: a !== a && b !== b (faster than Number.isNaN)
  • Prototype-based type checking: Object.getPrototypeOf(a) !== Object.getPrototypeOf(b) before constructor checks
  • Variable reuse: parameters a/b may be reassigned locally β€” intentional, not a bug
  • Cached prototype methods: const {valueOf, toString} = Object.prototype at module scope
  • oxlint-disable comments are intentional β€” typescript/unbound-method on the prototype method cache, typescript/no-restricted-types on the WeakMap<object, object> signature, no-self-compare on the NaN checks

Testing

  • Fixture-driven: Tests defined as TestSuite[] in src/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 TestCase entries to the appropriate suite in src/fixtures/tests.ts β€” include both equal and not-equal cases
  • Edge cases: Standalone it() blocks in is-equal.test.ts for circular refs, null-prototype, NaN

Gotchas

  • Sets use reference equality β€” new Set([{a:1}]) vs new Set([{a:1}]) returns false. 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() then toString() 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/byteLength slice. Byte-level comparison preserves NaN bit patterns
This project uses semantic-release with Angular preset (Conventional Commits). Commit messages directly control automated versioning:
  • fix: β†’ patch release
  • feat: β†’ minor release
  • BREAKING 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.