fast-equals 4.0.3 → 5.0.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/.babelrc +34 -0
  2. package/.prettierrc +4 -0
  3. package/CHANGELOG.md +39 -0
  4. package/README.md +145 -39
  5. package/build/rollup/config.base.js +49 -0
  6. package/build/rollup/config.cjs.js +10 -0
  7. package/build/rollup/config.esm.js +10 -0
  8. package/build/rollup/config.min.js +13 -0
  9. package/build/rollup/config.umd.js +10 -0
  10. package/build/tsconfig/base.json +32 -0
  11. package/build/tsconfig/cjs.json +8 -0
  12. package/build/tsconfig/declarations.json +9 -0
  13. package/build/tsconfig/esm.json +8 -0
  14. package/build/tsconfig/min.json +8 -0
  15. package/build/tsconfig/umd.json +8 -0
  16. package/build/webpack.config.js +57 -0
  17. package/dist/cjs/index.cjs +510 -0
  18. package/dist/cjs/index.cjs.map +1 -0
  19. package/dist/cjs/types/comparator.d.ts +2 -0
  20. package/dist/cjs/types/equals.d.ts +46 -0
  21. package/dist/cjs/types/index.d.ts +56 -0
  22. package/dist/cjs/types/internalTypes.d.ts +36 -0
  23. package/dist/cjs/types/utils.d.ts +19 -0
  24. package/dist/esm/index.mjs +499 -0
  25. package/dist/esm/index.mjs.map +1 -0
  26. package/dist/esm/types/comparator.d.ts +2 -0
  27. package/dist/esm/types/equals.d.ts +46 -0
  28. package/dist/esm/types/index.d.ts +56 -0
  29. package/dist/esm/types/internalTypes.d.ts +36 -0
  30. package/dist/esm/types/utils.d.ts +19 -0
  31. package/dist/min/index.js +1 -0
  32. package/dist/min/types/comparator.d.ts +2 -0
  33. package/dist/min/types/equals.d.ts +46 -0
  34. package/dist/min/types/index.d.ts +56 -0
  35. package/dist/min/types/internalTypes.d.ts +36 -0
  36. package/dist/min/types/utils.d.ts +19 -0
  37. package/dist/umd/index.js +516 -0
  38. package/dist/umd/index.js.map +1 -0
  39. package/dist/umd/types/comparator.d.ts +2 -0
  40. package/dist/umd/types/equals.d.ts +46 -0
  41. package/dist/umd/types/index.d.ts +56 -0
  42. package/dist/umd/types/internalTypes.d.ts +36 -0
  43. package/dist/umd/types/utils.d.ts +19 -0
  44. package/package.json +58 -37
  45. package/recipes/explicit-property-check.md +4 -6
  46. package/recipes/legacy-circular-equal-support.md +28 -20
  47. package/recipes/legacy-regexp-support.md +15 -16
  48. package/recipes/non-standard-properties.md +35 -23
  49. package/recipes/using-meta-in-comparison.md +10 -13
  50. package/src/comparator.ts +43 -40
  51. package/src/equals.ts +263 -0
  52. package/src/index.ts +183 -79
  53. package/src/internalTypes.ts +74 -0
  54. package/src/utils.ts +42 -49
  55. package/dist/fast-equals.cjs.js +0 -432
  56. package/dist/fast-equals.cjs.js.map +0 -1
  57. package/dist/fast-equals.esm.js +0 -422
  58. package/dist/fast-equals.esm.js.map +0 -1
  59. package/dist/fast-equals.js +0 -438
  60. package/dist/fast-equals.js.map +0 -1
  61. package/dist/fast-equals.min.js +0 -1
  62. package/dist/fast-equals.mjs +0 -422
  63. package/dist/fast-equals.mjs.map +0 -1
  64. package/index.d.ts +0 -58
  65. package/recipes/strict-property-descriptor-check.md +0 -42
  66. package/src/arrays.ts +0 -36
  67. package/src/dates.ts +0 -12
  68. package/src/maps.ts +0 -66
  69. package/src/objects.ts +0 -62
  70. package/src/regexps.ts +0 -11
  71. package/src/sets.ts +0 -61
@@ -0,0 +1,56 @@
1
+ import type { CreateCustomComparatorConfig, CreateState, EqualityComparator } from './internalTypes';
2
+ import { createInternalComparator, sameValueZeroEqual } from './utils';
3
+ export { sameValueZeroEqual };
4
+ interface DefaultEqualCreatorOptions<Meta> {
5
+ comparator?: EqualityComparator<Meta>;
6
+ circular?: boolean;
7
+ strict?: boolean;
8
+ }
9
+ interface CustomEqualCreatorOptions<Meta> extends DefaultEqualCreatorOptions<Meta> {
10
+ createCustomConfig?: CreateCustomComparatorConfig<Meta>;
11
+ createInternalComparator?: typeof createInternalComparator;
12
+ createState?: CreateState<Meta>;
13
+ }
14
+ /**
15
+ * Whether the items passed are deeply-equal in value.
16
+ */
17
+ export declare const deepEqual: <A, B>(a: A, b: B) => boolean;
18
+ /**
19
+ * Whether the items passed are deeply-equal in value based on strict comparison.
20
+ */
21
+ export declare const strictDeepEqual: <A, B>(a: A, b: B) => boolean;
22
+ /**
23
+ * Whether the items passed are deeply-equal in value, including circular references.
24
+ */
25
+ export declare const circularDeepEqual: <A, B>(a: A, b: B) => boolean;
26
+ /**
27
+ * Whether the items passed are deeply-equal in value, including circular references,
28
+ * based on strict comparison.
29
+ */
30
+ export declare const strictCircularDeepEqual: <A, B>(a: A, b: B) => boolean;
31
+ /**
32
+ * Whether the items passed are shallowly-equal in value.
33
+ */
34
+ export declare const shallowEqual: <A, B>(a: A, b: B) => boolean;
35
+ /**
36
+ * Whether the items passed are shallowly-equal in value based on strict comparison
37
+ */
38
+ export declare const strictShallowEqual: <A, B>(a: A, b: B) => boolean;
39
+ /**
40
+ * Whether the items passed are shallowly-equal in value, including circular references.
41
+ */
42
+ export declare const circularShallowEqual: <A, B>(a: A, b: B) => boolean;
43
+ /**
44
+ * Whether the items passed are shallowly-equal in value, including circular references,
45
+ * based on strict comparison.
46
+ */
47
+ export declare const strictCircularShallowEqual: <A, B>(a: A, b: B) => boolean;
48
+ /**
49
+ * Create a custom equality comparison method.
50
+ *
51
+ * This can be done to create very targeted comparisons in extreme hot-path scenarios
52
+ * where the standard methods are not performant enough, but can also be used to provide
53
+ * support for legacy environments that do not support expected features like
54
+ * `RegExp.prototype.flags` out of the box.
55
+ */
56
+ export declare function createCustomEqual<Meta>(options?: CustomEqualCreatorOptions<Meta>): <A, B>(a: A, b: B, metaOverride?: Meta) => boolean;
@@ -0,0 +1,36 @@
1
+ export interface BaseCircular extends Pick<WeakMap<any, any>, 'delete' | 'get'> {
2
+ set(key: object, value: any): any;
3
+ }
4
+ export type State<Meta> = CircularState<Meta> | DefaultState<Meta>;
5
+ export interface CircularState<Meta> {
6
+ readonly cache: BaseCircular;
7
+ readonly equals: InternalEqualityComparator<Meta>;
8
+ meta: Meta;
9
+ readonly strict: boolean;
10
+ }
11
+ export interface DefaultState<Meta> {
12
+ readonly cache: undefined;
13
+ readonly equals: InternalEqualityComparator<Meta>;
14
+ meta: Meta;
15
+ readonly strict: boolean;
16
+ }
17
+ export interface Dictionary<Value = any> {
18
+ [key: string | symbol]: Value;
19
+ $$typeof?: any;
20
+ }
21
+ export interface ComparatorConfig<Meta> {
22
+ areArraysEqual: TypeEqualityComparator<any, Meta>;
23
+ areDatesEqual: TypeEqualityComparator<any, Meta>;
24
+ areMapsEqual: TypeEqualityComparator<any, Meta>;
25
+ areObjectsEqual: TypeEqualityComparator<any, Meta>;
26
+ arePrimitiveWrappersEqual: TypeEqualityComparator<any, Meta>;
27
+ areRegExpsEqual: TypeEqualityComparator<any, Meta>;
28
+ areSetsEqual: TypeEqualityComparator<any, Meta>;
29
+ }
30
+ export type CreateCustomComparatorConfig<Meta> = (config: ComparatorConfig<Meta>) => Partial<ComparatorConfig<Meta>>;
31
+ export type CreateState<Meta> = (comparator: EqualityComparator<Meta>) => Partial<State<Meta>>;
32
+ export type EqualityComparator<Meta> = <A, B>(a: A, b: B, state: State<Meta>) => boolean;
33
+ export type AnyEqualityComparator<Meta> = (a: any, b: any, state: State<Meta>) => boolean;
34
+ export type EqualityComparatorCreator<Meta> = (fn: EqualityComparator<Meta>) => InternalEqualityComparator<Meta>;
35
+ export type InternalEqualityComparator<Meta> = (a: any, b: any, indexOrKeyA: any, indexOrKeyB: any, parentA: any, parentB: any, state: State<Meta>) => boolean;
36
+ export type TypeEqualityComparator<Type, Meta = undefined> = (a: Type, b: Type, state: State<Meta>) => boolean;
@@ -0,0 +1,19 @@
1
+ import { AnyEqualityComparator, Dictionary, EqualityComparator, InternalEqualityComparator, State, TypeEqualityComparator } from './internalTypes';
2
+ export declare function combineComparators<Meta>(comparatorA: AnyEqualityComparator<Meta>, comparatorB: AnyEqualityComparator<Meta>): <A, B>(a: A, b: B, state: State<Meta>) => boolean;
3
+ /**
4
+ * Default equality comparator pass-through, used as the standard `isEqual` creator for
5
+ * use inside the built comparator.
6
+ */
7
+ export declare function createInternalComparator<Meta>(compare: EqualityComparator<Meta>): InternalEqualityComparator<Meta>;
8
+ /**
9
+ * Wrap the provided `areItemsEqual` method to manage the circular state, allowing
10
+ * for circular references to be safely included in the comparison without creating
11
+ * stack overflows.
12
+ */
13
+ export declare function createIsCircular<AreItemsEqual extends TypeEqualityComparator<any, any>>(areItemsEqual: AreItemsEqual): AreItemsEqual;
14
+ export declare function getStrictProperties(object: Dictionary): Array<string | symbol>;
15
+ export declare const hasOwn: (o: object, v: PropertyKey) => boolean;
16
+ /**
17
+ * Whether the values passed are strictly equal or both NaN.
18
+ */
19
+ export declare function sameValueZeroEqual(a: any, b: any): boolean;
package/package.json CHANGED
@@ -1,53 +1,69 @@
1
1
  {
2
2
  "author": "tony_quetano@planttheidea.com",
3
- "browser": "dist/fast-equals.js",
3
+ "browser": "dist/umd/index.js",
4
4
  "bugs": {
5
5
  "url": "https://github.com/planttheidea/fast-equals/issues"
6
6
  },
7
7
  "description": "A blazing fast equality comparison, either shallow or deep",
8
8
  "devDependencies": {
9
- "@rollup/plugin-node-resolve": "^13.1.3",
10
- "@types/jest": "^28.1.8",
9
+ "@rollup/plugin-commonjs": "^24.0.0",
10
+ "@rollup/plugin-node-resolve": "^15.0.1",
11
+ "@rollup/plugin-replace": "^5.0.2",
12
+ "@rollup/plugin-terser": "^0.4.0",
13
+ "@rollup/plugin-typescript": "^11.0.0",
14
+ "@types/jest": "^29.2.5",
11
15
  "@types/lodash": "^4.14.184",
12
- "@types/node": "^18.7.13",
13
- "@types/ramda": "^0.28.15",
14
- "@types/react": "^18.0.17",
15
- "@typescript-eslint/eslint-plugin": "^5.35.1",
16
- "@typescript-eslint/parser": "^5.35.1",
17
- "benchee": "^1.1.0",
18
- "cli-table3": "^0.6.1",
16
+ "@types/node": "^18.14.0",
17
+ "@types/ramda": "^0.28.23",
18
+ "@types/react": "^18.0.28",
19
+ "@types/react-dom": "^18.0.11",
20
+ "@typescript-eslint/eslint-plugin": "^5.52.0",
21
+ "@typescript-eslint/parser": "^5.52.0",
19
22
  "decircularize": "^1.0.0",
20
23
  "deep-eql": "^4.1.0",
21
24
  "deep-equal": "^2.0.5",
22
- "eslint": "^8.22.0",
23
- "eslint-config-airbnb": "^19.0.4",
24
- "eslint-plugin-import": "^2.25.4",
25
- "eslint-plugin-jsx-a11y": "^6.6.1",
26
- "eslint-plugin-react": "^7.31.0",
27
- "eslint-webpack-plugin": "^3.2.0",
25
+ "eslint": "^8.34.0",
26
+ "eslint-friendly-formatter": "^4.0.1",
27
+ "eslint-webpack-plugin": "^4.0.0",
28
28
  "fast-deep-equal": "^3.1.3",
29
- "fs-extra": "^10.0.0",
30
29
  "html-webpack-plugin": "^5.5.0",
31
30
  "in-publish": "^2.0.0",
32
- "jest": "^28.1.3",
31
+ "jest": "^29.4.3",
32
+ "jest-environment-jsdom": "^29.4.3",
33
+ "jest-expect-message": "^1.1.3",
33
34
  "lodash": "^4.17.21",
34
35
  "nano-equal": "^2.0.2",
35
- "prettier": "^2.7.1",
36
+ "prettier": "^2.8.4",
36
37
  "react": "^18.2.0",
37
38
  "react-dom": "^18.2.0",
38
39
  "react-fast-compare": "^3.2.0",
39
- "release-it": "^15.4.0",
40
- "rollup": "^2.78.1",
41
- "rollup-plugin-terser": "^7.0.2",
42
- "rollup-plugin-typescript2": "^0.33.0",
40
+ "release-it": "^15.6.0",
41
+ "rollup": "^3.16.0",
43
42
  "shallow-equal-fuzzy": "^0.0.2",
44
- "ts-jest": "^28.0.8",
45
- "ts-loader": "^9.3.1",
46
- "typescript": "^4.7.4",
43
+ "tinybench": "^2.3.1",
44
+ "ts-jest": "^29.0.3",
45
+ "ts-loader": "^9.4.2",
46
+ "typescript": "^4.9.5",
47
47
  "underscore": "^1.13.4",
48
- "webpack": "^5.74.0",
49
- "webpack-cli": "^4.10.0",
50
- "webpack-dev-server": "^4.10.0"
48
+ "webpack": "^5.75.0",
49
+ "webpack-cli": "^5.0.1",
50
+ "webpack-dev-server": "^4.11.1"
51
+ },
52
+ "exports": {
53
+ ".": {
54
+ "import": {
55
+ "types": "./dist/esm/types/index.d.ts",
56
+ "default": "./dist/esm/index.mjs"
57
+ },
58
+ "require": {
59
+ "types": "./dist/cjs/types/index.d.ts",
60
+ "default": "./dist/cjs/index.cjs"
61
+ },
62
+ "default": {
63
+ "types": "./dist/umd/types/index.d.ts",
64
+ "default": "./dist/umd/index.js"
65
+ }
66
+ }
51
67
  },
52
68
  "homepage": "https://github.com/planttheidea/fast-equals#readme",
53
69
  "keywords": [
@@ -58,32 +74,37 @@
58
74
  "equivalent"
59
75
  ],
60
76
  "license": "MIT",
61
- "main": "dist/fast-equals.cjs.js",
62
- "module": "dist/fast-equals.esm.js",
77
+ "main": "dist/cjs/index.cjs",
78
+ "module": "dist/esm/index.mjs",
63
79
  "name": "fast-equals",
64
80
  "repository": {
65
81
  "type": "git",
66
82
  "url": "git+https://github.com/planttheidea/fast-equals.git"
67
83
  },
68
84
  "scripts": {
69
- "benchmark": "npm run dist && node benchmark/index.js",
70
- "build": "NODE_ENV=production rollup -c",
71
- "dev": "NODE_ENV=development webpack serve --progress --config=webpack/webpack.config.js",
85
+ "benchmark": "npm run build:esm && node benchmark/index.js",
86
+ "build": "npm run build:esm && npm run build:cjs && npm run build:umd && npm run build:min",
87
+ "build:cjs": "rimraf dist/cjs && NODE_ENV=production rollup -c build/rollup/config.cjs.js && tsc -p ./build/tsconfig/cjs.json",
88
+ "build:esm": "rimraf dist/esm && NODE_ENV=production rollup -c build/rollup/config.esm.js && tsc -p ./build/tsconfig/esm.json",
89
+ "build:min": "rimraf dist/min && NODE_ENV=production rollup -c build/rollup/config.min.js && tsc -p ./build/tsconfig/min.json",
90
+ "build:umd": "rimraf dist/umd && NODE_ENV=production rollup -c build/rollup/config.umd.js && tsc -p ./build/tsconfig/umd.json",
91
+ "dev": "NODE_ENV=development webpack serve --progress --config=build/webpack.config.js",
72
92
  "dist": "rimraf dist && npm run build",
73
93
  "format": "prettier **/*.ts --write",
74
94
  "lint": "eslint src/*.ts",
75
95
  "lint:fix": "npm run lint -- --fix",
76
96
  "start": "npm run dev",
77
97
  "prepublish": "if in-publish; then npm run prepublish:compile; fi",
78
- "prepublish:compile": "npm run typecheck && npm run lint && npm run test && npm run dist",
79
98
  "release": "release-it",
80
99
  "release:beta": "release-it --config=.release-it.beta.json",
100
+ "release:scripts": "npm run typecheck && npm run lint && npm run test && npm run dist",
81
101
  "test": "NODE_PATH=. jest",
82
- "test:coverage": "npm test -- --coverage",
102
+ "test:coverage": "rimraf coverage && npm test -- --coverage",
83
103
  "test:watch": "npm test -- --watch",
84
104
  "typecheck": "tsc --noEmit"
85
105
  },
86
106
  "sideEffects": false,
107
+ "type": "module",
87
108
  "types": "index.d.ts",
88
- "version": "4.0.3"
109
+ "version": "5.0.0-beta.0"
89
110
  }
@@ -18,11 +18,9 @@ interface SpecialObject {
18
18
  const areObjectsEqual: TypeEqualityComparator<SpecialObject, undefined> = (
19
19
  a,
20
20
  b,
21
- ) => {
22
- return a.foo === b.foo && a.bar.baz === b.bar.baz;
23
- };
21
+ ) => a.foo === b.foo && a.bar.baz === b.bar.baz;
24
22
 
25
- const isSpecialObjectEqual = createCustomEqual(() => ({
26
- areObjectsEqual,
27
- }));
23
+ const isSpecialObjectEqual = createCustomEqual({
24
+ createCustomConfig: () => ({ areObjectsEqual }),
25
+ });
28
26
  ```
@@ -3,14 +3,9 @@
3
3
  Starting in `4.x.x`, `WeakMap` is expected to be available in the environment. All modern browsers support this global object, however there may be situations where a legacy environmental support is required (example: IE11). If you need to support such an environment, creating a custom comparator that uses a custom cache implementation with the same contract is a simple solution.
4
4
 
5
5
  ```ts
6
- import { createCustomEqual, sameValueZeroEqual } from 'deep-Equals';
6
+ import { createCustomEqual, sameValueZeroEqual } from 'fast-equals';
7
7
 
8
- interface Cache extends BaseCircularMeta {
9
- customMethod(): void;
10
- customValue: string;
11
- }
12
-
13
- function getCache(): Cache {
8
+ function getCache(): BaseCircular {
14
9
  const entries: Array<[object, any]> = [];
15
10
 
16
11
  return {
@@ -45,22 +40,35 @@ function getCache(): Cache {
45
40
 
46
41
  return this;
47
42
  },
48
-
49
- customMethod() {
50
- console.log('hello!');
51
- },
52
- customValue: 'goodbye',
53
43
  };
54
44
  }
55
45
 
56
- const customDeepCircularHandler = createCustomCircularEqual<Cache>(() => ({}));
46
+ interface Meta {
47
+ customMethod(): void;
48
+ customValue: string;
49
+ }
50
+
51
+ const meta = {
52
+ customMethod() {
53
+ console.log('hello!');
54
+ },
55
+ customValue: 'goodbye',
56
+ };
57
57
 
58
- const customDeepCircularHandler = createCustomCircularEqual<Cache>(() => ({
59
- createIsNestedEqual: () => sameValueZeroEqual,
60
- }));
58
+ const circularDeepEqual = createCustomEqual<Cache>({
59
+ circular: true,
60
+ createState: () => ({
61
+ cache: getCache(),
62
+ meta,
63
+ }),
64
+ });
61
65
 
62
- const circularDeepEqual = <A, B>(a: A, b: B) =>
63
- customDeepCircularHandler(a, b, getCache());
64
- const circularShallowEqual = <A, B>(a: A, b: B) =>
65
- customShallowCircularHandler(a, b, getCache());
66
+ const circularShallowEqual = createCustomEqual<Cache>({
67
+ circular: true,
68
+ comparator: sameValueZeroEqual,
69
+ createState: () => ({
70
+ cache: getCache(),
71
+ meta,
72
+ }),
73
+ });
66
74
  ```
@@ -6,21 +6,20 @@ Starting in `4.x.x`, `RegExp.prototype.flags` is expected to be available in the
6
6
  import { createCustomEqual, sameValueZeroEqual } from 'deep-Equals';
7
7
  import type { TypeEqualityComparator } from 'fast-equals';
8
8
 
9
- const areRegExpsEqual: TypeEqualityComparator<RegExp, undefined> = (a, b) => {
10
- return (
11
- a.source === b.source &&
12
- a.global === b.global &&
13
- a.ignoreCase === b.ignoreCase &&
14
- a.multiline === b.multiline &&
15
- a.unicode === b.unicode &&
16
- a.sticky === b.sticky &&
17
- a.lastIndex === b.lastIndex
18
- );
19
- };
9
+ const areRegExpsEqual: TypeEqualityComparator<RegExp, undefined> = (a, b) =>
10
+ a.source === b.source &&
11
+ a.global === b.global &&
12
+ a.ignoreCase === b.ignoreCase &&
13
+ a.multiline === b.multiline &&
14
+ a.unicode === b.unicode &&
15
+ a.sticky === b.sticky &&
16
+ a.lastIndex === b.lastIndex;
20
17
 
21
- const deepEqual = createCustomEqual(() => ({ areRegExpsEqual }));
22
- const shallowEqual = createCustomEqual(() => ({
23
- areRegExpsEqual,
24
- createIsNestedEqual: () => sameValueZeroEqual,
25
- }));
18
+ const deepEqual = createCustomEqual({
19
+ createCustomConfig: () => ({ areRegExpsEqual }),
20
+ });
21
+ const shallowEqual = createCustomEqual({
22
+ comparator: sameValueZero,
23
+ createCustomConfig: () => ({ areRegExpsEqual }),
24
+ });
26
25
  ```
@@ -1,34 +1,46 @@
1
1
  # Non-standard properties
2
2
 
3
- Sometimes, objects require a comparison that extend beyond its own keys. Perhaps there is a non-enumerable property that is important, or perhaps there are symbols as keys. In this case, the standard validators will return false positives, because internally `fast-equals` uses `Object.keys()` for object comparisons.
4
-
5
- Using a custom object comparator with `createCustomEqual` allows these kinds of comparisons.
3
+ Sometimes, objects require a comparison that extend beyond its own keys, or even its own properties or symbols. Using a custom object comparator with `createCustomEqual` allows these kinds of comparisons.
6
4
 
7
5
  ```ts
8
6
  import { createCustomEqual } from 'fast-equals';
9
7
  import type { TypeEqualityComparator } from 'fast-equals';
10
8
 
11
- const areObjectsEqual: TypeEqualityComparator<Record<any, any>, undefined> = (
12
- a,
13
- b,
14
- ) => {
15
- const propertiesA = [
16
- ...Object.getOwnPropertyNames(a),
17
- ...Object.getOwnPropertySymbols(a),
18
- ];
19
- const propertiesB = [
20
- ...Object.getOwnPropertyNames(b),
21
- ...Object.getOwnPropertySymbols(b),
22
- ];
23
-
24
- if (propertiesA.length !== propertiesB.length) {
25
- return false;
9
+ class HiddenProperty {
10
+ visible: boolean;
11
+ #hidden: string;
12
+
13
+ constructor(value: string) {
14
+ this.visible = true;
15
+ this.#hidden = value;
16
+ }
17
+
18
+ get hidden() {
19
+ return this.#hidden;
26
20
  }
21
+ }
22
+
23
+ function createAreObjectsEqual(
24
+ areObjectsEqual: TypeEqualityComparator<Record<any, any>, undefined>,
25
+ ): TypeEqualityComparator<Record<any, any>, undefined> {
26
+ return function (a, b, state) {
27
+ if (!areObjectsEqual(a, b, state)) {
28
+ return false;
29
+ }
30
+
31
+ const aInstance = a instanceof HiddenProperty;
32
+ const bInstance = b instanceof HiddenProperty;
33
+
34
+ if (aInstance || bInstance) {
35
+ return aInstance && bInstance && a.hidden === b.hidden;
36
+ }
27
37
 
28
- return propertiesA.every(
29
- (property) => a[property as any] === b[property as any],
30
- );
31
- };
38
+ return true;
39
+ };
40
+ }
32
41
 
33
- const deepEqual = createCustomEqual(() => ({ areObjectsEqual }));
42
+ const deepEqual = createCustomEqual({
43
+ createCustomConfig: ({ areObjectsEqual }) =>
44
+ createAreObjectsEqual(areObjectsEqual),
45
+ });
34
46
  ```
@@ -1,23 +1,20 @@
1
1
  # Using `meta` in comparison
2
2
 
3
- Sometimes a "pure" equality between two objects is insufficient, because the comparison relies on some external state. While these kinds of scenarios should generally be avoided, it is possible to handle them with a custom comparator that checks `meta` values.
3
+ Sometimes a "pure" equality between two objects is insufficient, because the comparison relies on some external state. While these kinds of scenarios should generally be avoided, it is possible to handle them with a custom internal comparator that checks `meta` values.
4
4
 
5
5
  ```ts
6
6
  import { createCustomEqual } from 'fast-equals';
7
- import type {
8
- EqualityComparator,
9
- InternalEqualityComparator,
10
- } from 'fast-equals';
11
7
 
12
- interface MutableState {
13
- state: string;
8
+ interface Meta {
9
+ value: string;
14
10
  }
15
11
 
16
- const mutableState: MutableState = { state: 'baz' };
12
+ const meta: Meta = { value: 'baz' };
17
13
 
18
- const createIsNestedEqual: EqualityComparatorCreator<MutableState> =
19
- (deepEqual) => (a, b, keyA, keyB, parentA, parentB, meta) =>
20
- deepEqual(a, b, meta) || a === meta.state || b === meta.state;
21
-
22
- const deepEqual = createCustomEqual(() => ({ createIsNestedEqual }));
14
+ const deepEqual = createCustomEqual<Meta>({
15
+ createInternalComparator:
16
+ (compare) => (a, b, _keyA, _keyB, _parentA, _parentB, state) =>
17
+ compare(a, b, state) || a === state.meta.value || b === state.meta.value,
18
+ createState: () => ({ meta }),
19
+ });
23
20
  ```
package/src/comparator.ts CHANGED
@@ -1,37 +1,37 @@
1
- import { isPlainObject, isPromiseLike, sameValueZeroEqual } from './utils';
2
-
3
1
  import type {
4
- CreateComparatorCreatorOptions,
2
+ ComparatorConfig,
5
3
  EqualityComparator,
6
- } from '../index.d';
4
+ State,
5
+ } from './internalTypes';
7
6
 
8
7
  const ARGUMENTS_TAG = '[object Arguments]';
9
8
  const BOOLEAN_TAG = '[object Boolean]';
10
9
  const DATE_TAG = '[object Date]';
11
- const REG_EXP_TAG = '[object RegExp]';
12
10
  const MAP_TAG = '[object Map]';
13
11
  const NUMBER_TAG = '[object Number]';
14
12
  const OBJECT_TAG = '[object Object]';
13
+ const REG_EXP_TAG = '[object RegExp]';
15
14
  const SET_TAG = '[object Set]';
16
15
  const STRING_TAG = '[object String]';
17
16
 
18
- const { toString } = Object.prototype;
17
+ const { isArray } = Array;
18
+ const getTag = Object.prototype.toString.call.bind(
19
+ Object.prototype.toString,
20
+ ) as (a: object) => string;
19
21
 
20
22
  export function createComparator<Meta>({
21
23
  areArraysEqual,
22
24
  areDatesEqual,
23
25
  areMapsEqual,
24
26
  areObjectsEqual,
27
+ arePrimitiveWrappersEqual,
25
28
  areRegExpsEqual,
26
29
  areSetsEqual,
27
- createIsNestedEqual,
28
- }: CreateComparatorCreatorOptions<Meta>): EqualityComparator<Meta> {
29
- const isEqual = createIsNestedEqual(comparator as EqualityComparator<Meta>);
30
-
30
+ }: ComparatorConfig<Meta>): EqualityComparator<Meta> {
31
31
  /**
32
32
  * compare the value of the two objects and return true if they are equivalent in values
33
33
  */
34
- function comparator(a: any, b: any, meta: Meta): boolean {
34
+ return function comparator(a: any, b: any, state: State<Meta>): boolean {
35
35
  // If the items are strictly equal, no need to do a value comparison.
36
36
  if (a === b) {
37
37
  return true;
@@ -57,64 +57,69 @@ export function createComparator<Meta>({
57
57
  // `isPlainObject` only checks against the object's own realm. Cross-realm
58
58
  // comparisons are rare, and will be handled in the ultimate fallback, so
59
59
  // we can avoid the `toString.call()` cost unless necessary.
60
- if (isPlainObject(a) && isPlainObject(b)) {
61
- return areObjectsEqual(a, b, isEqual, meta);
60
+ if (a.constructor === Object && b.constructor === Object) {
61
+ return areObjectsEqual(a, b, state);
62
62
  }
63
63
 
64
64
  // `isArray()` works on subclasses and is cross-realm, so we can again avoid
65
65
  // the `toString.call()` cost unless necessary by just checking if either
66
66
  // and then both are arrays.
67
- const aArray = Array.isArray(a);
68
- const bArray = Array.isArray(b);
67
+ const aArray = isArray(a);
68
+ const bArray = isArray(b);
69
69
 
70
70
  if (aArray || bArray) {
71
- return aArray === bArray && areArraysEqual(a, b, isEqual, meta);
71
+ return aArray === bArray && areArraysEqual(a, b, state);
72
72
  }
73
73
 
74
74
  // Since this is a custom object, use the classic `toString.call()` to get its
75
75
  // type. This is reasonably performant in modern environments like v8 and
76
76
  // SpiderMonkey, and allows for cross-realm comparison when other checks like
77
77
  // `instanceof` do not.
78
- const aTag = toString.call(a);
78
+ const tag = getTag(a);
79
79
 
80
- if (aTag !== toString.call(b)) {
80
+ if (tag !== getTag(b)) {
81
81
  return false;
82
82
  }
83
83
 
84
- if (aTag === DATE_TAG) {
85
- // `getTime()` showed better results compared to alternatives like `valueOf()`
86
- // or the unary `+` operator.
87
- return areDatesEqual(a, b, isEqual, meta);
84
+ if (tag === DATE_TAG) {
85
+ return areDatesEqual(a, b, state);
88
86
  }
89
87
 
90
- if (aTag === REG_EXP_TAG) {
91
- return areRegExpsEqual(a, b, isEqual, meta);
88
+ if (tag === REG_EXP_TAG) {
89
+ return areRegExpsEqual(a, b, state);
92
90
  }
93
91
 
94
- if (aTag === MAP_TAG) {
95
- return areMapsEqual(a, b, isEqual, meta);
92
+ if (tag === MAP_TAG) {
93
+ return areMapsEqual(a, b, state);
96
94
  }
97
95
 
98
- if (aTag === SET_TAG) {
99
- return areSetsEqual(a, b, isEqual, meta);
96
+ if (tag === SET_TAG) {
97
+ return areSetsEqual(a, b, state);
100
98
  }
101
99
 
102
100
  // If a simple object tag, then we can prioritize a simple object comparison because
103
- // it is likely a custom class. If an arguments tag, it should be treated as a standard
104
- // object.
105
- if (aTag === OBJECT_TAG || aTag === ARGUMENTS_TAG) {
101
+ // it is likely a custom class.
102
+ if (tag === OBJECT_TAG) {
106
103
  // The exception for value comparison is `Promise`-like contracts. These should be
107
- // treated the same as standard `Promise` objects, which means strict equality.
108
- return isPromiseLike(a) || isPromiseLike(b)
109
- ? false
110
- : areObjectsEqual(a, b, isEqual, meta);
104
+ // treated the same as standard `Promise` objects, which means strict equality, and if
105
+ // it reaches this point then that strict equality comparison has already failed.
106
+ return (
107
+ typeof a.then !== 'function' &&
108
+ typeof b.then !== 'function' &&
109
+ areObjectsEqual(a, b, state)
110
+ );
111
+ }
112
+
113
+ // If an arguments tag, it should be treated as a standard object.
114
+ if (tag === ARGUMENTS_TAG) {
115
+ return areObjectsEqual(a, b, state);
111
116
  }
112
117
 
113
118
  // As the penultimate fallback, check if the values passed are primitive wrappers. This
114
119
  // is very rare in modern JS, which is why it is deprioritized compared to all other object
115
120
  // types.
116
- if (aTag === BOOLEAN_TAG || aTag === NUMBER_TAG || aTag === STRING_TAG) {
117
- return sameValueZeroEqual(a.valueOf(), b.valueOf());
121
+ if (tag === BOOLEAN_TAG || tag === NUMBER_TAG || tag === STRING_TAG) {
122
+ return arePrimitiveWrappersEqual(a, b, state);
118
123
  }
119
124
 
120
125
  // If not matching any tags that require a specific type of comparison, then we hard-code false because
@@ -129,7 +134,5 @@ export function createComparator<Meta>({
129
134
  // In all cases, these decisions should be reevaluated based on changes to the language and
130
135
  // common development practices.
131
136
  return false;
132
- }
133
-
134
- return comparator as EqualityComparator<Meta>;
137
+ };
135
138
  }