diff --git a/__tests__/comparator.ts b/__tests__/comparator.ts index ed726e9..1308d79 100644 --- a/__tests__/comparator.ts +++ b/__tests__/comparator.ts @@ -25,6 +25,7 @@ const STANDARD_COMPARATOR_OPTIONS = { areRegExpsEqual, areSetsEqual, areTypedArraysEqual, + unknownTagComparators: {}, }; const CIRCULAR_COMPARATOR_OPTIONS = { ...STANDARD_COMPARATOR_OPTIONS, diff --git a/__tests__/recipes.ts b/__tests__/recipes.ts index a12270d..1ef04c4 100644 --- a/__tests__/recipes.ts +++ b/__tests__/recipes.ts @@ -249,6 +249,38 @@ describe('recipes', () => { expect(deepEqual(a, c)).toBe(false); }); }); + + describe("special-objects", () => { + it("should call unknownTagComparators", () => { + // Emulate something like a private property + const hiddenData = new WeakMap(); + const tag = "Thing"; + + class Thing { + [Symbol.toStringTag] = tag + constructor(value: number) { + hiddenData.set(this, value); + } + equals(other: Thing) { + return hiddenData.get(this) === hiddenData.get(other); + } + } + + const deepEqual = createCustomEqual({ + createCustomConfig: () => ({ + unknownTagComparators: { + [tag]: (a, b) => a instanceof Thing && b instanceof Thing && a.equals(b) + } + }), + }); + + const a = new Thing(1); + const b = new Thing(1); + const c = new Thing(2); + expect(deepEqual(a, b)).toBe(true); + expect(deepEqual(a, c)).toBe(false); + }); + }); }); describe('createCustomCircularEqual', () => { diff --git a/recipes/special-objects.md b/recipes/special-objects.md new file mode 100644 index 0000000..a2b3c75 --- /dev/null +++ b/recipes/special-objects.md @@ -0,0 +1,25 @@ +# Handling special (built-in) objects + +The `createCustomEqual` uses `@@toStringTag` to decide which well-known comparator to call. However, some values might not fit either of them. For example `WeakMap` is compared intentionally. Additionally, it's possible that new built-in objects are added to the language. Third parties might also add `@@toStringTag` to their objects. + +For example the [TC39 Temporal spec](https://tc39.es/proposal-temporal/docs/) requires `@@toStringTag` being implemetend and the polyfills do that. Passing `areObjectsEqual` will not work in that scenario. Instead you'll have to register a handler for the tag instead. + +```ts +import { createCustomEqual } from 'fast-equals'; +import type { TypeEqualityComparator } from 'fast-equals'; + +const areZonedDateTimesEqual: TypeEqualityComparator = ( + a, + b, +) => a instanceof Temporal.ZonedDateTime + && b instanceof Temporal.ZonedDateTime + && a.equals(b); + +const isSpecialObjectEqual = createCustomEqual({ + createCustomConfig: () => ({ + unknownTagComparators: { + "Temporal.ZonedDateTime", areZonedDateTimesEqual, + } + }), +}); +``` diff --git a/src/comparator.ts b/src/comparator.ts index a2a8be2..298e58d 100644 --- a/src/comparator.ts +++ b/src/comparator.ts @@ -38,6 +38,7 @@ const { assign } = Object; const getTag = Object.prototype.toString.call.bind( Object.prototype.toString, ) as (a: object) => string; +const getShortTag = (a: object): string | undefined => a != null ? (a as any)[Symbol.toStringTag] : undefined; interface CreateIsEqualOptions { circular: boolean; @@ -59,6 +60,7 @@ export function createEqualityComparator({ areRegExpsEqual, areSetsEqual, areTypedArraysEqual, + unknownTagComparators, }: ComparatorConfig): EqualityComparator { /** * compare the value of the two objects and return true if they are equivalent in values @@ -194,7 +196,8 @@ export function createEqualityComparator({ // equality is (`Error`, etc.), the subjective decision is to be conservative and strictly compare. // In all cases, these decisions should be reevaluated based on changes to the language and // common development practices. - return false; + const unknownTagComparator = unknownTagComparators[getShortTag(a) ?? tag]; + return unknownTagComparator ? unknownTagComparator(a, b, state) : false; }; } @@ -225,6 +228,7 @@ export function createEqualityComparatorConfig({ areTypedArraysEqual: strict ? areObjectsEqualStrictDefault : areTypedArraysEqual, + unknownTagComparators: {}, }; if (createCustomConfig) { diff --git a/src/internalTypes.ts b/src/internalTypes.ts index 58e1c31..3e082bb 100644 --- a/src/internalTypes.ts +++ b/src/internalTypes.ts @@ -80,6 +80,13 @@ export interface ComparatorConfig { * additional properties added to the typed array. */ areTypedArraysEqual: TypeEqualityComparator; + /** + * Whether two values with unknown `@@toStringTag` are equal in value. This comparator is + * called when no other comparator applies. + * + * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/toStringTag + */ + unknownTagComparators: Partial>>; } export type CreateCustomComparatorConfig = (