Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 56 additions & 7 deletions docs/typed/isEqual.mdx
Original file line number Diff line number Diff line change
@@ -1,20 +1,69 @@
---
title: isEqual
description: Determine if two values are equal
description: Determine if two values are deeply equal
since: 12.1.0
---

### Usage

Given two values, returns true if they are equal.
Returns `true` when two values are deeply equal. It starts with `Object.is()` and then performs additional comparisons for arrays, dates, regular expressions, and finally objects of any other type.

```ts
import * as _ from 'radashi'

_.isEqual(null, null) // => true
_.isEqual([], []) // => true
_.isEqual({ hello: 'world' }, { hello: 'world' }) // => true
const left = {
id: 1,
createdAt: new Date('2024-01-01T00:00:00.000Z'),
tags: ['radashi', 'typed'],
[Symbol.for('role')]: 'admin',
}

_.isEqual('hello', 'world') // => false
_.isEqual(22, 'abc') // => false
const right = {
tags: ['radashi', 'typed'],
createdAt: new Date('2024-01-01T00:00:00.000Z'),
id: 1,
[Symbol.for('role')]: 'admin',
}

_.isEqual(left, right) // => true

_.isEqual({ id: 1 }, { id: 2 }) // => false
_.isEqual([1, 2, 3], [1, 2, 4]) // => false
_.isEqual(/hello/gi, /hello/g) // => false
```

Arrays are compared by length and element order, objects are compared recursively (including symbol keys), dates compare their timestamps, and regular expressions compare both pattern and flags. Instances must share the same prototype, and objects from other [realms](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/instanceof#instanceof_and_multiple_realms) are unequal by default.

### Custom comparison

Pass a `customCompare` function to handle types beyond the built-in cases. The function receives both values and may return:

- `true` or `false` to override the result.
- `null`/`undefined` to fall back to the default behavior.

The custom function is only called once arrays, plain objects, dates, and regular expressions have been ruled out. This makes it ideal for handling `Map`, `Set`, and domain-specific classes.

```ts
const compareCollections = (left: unknown, right: unknown) => {
if (left instanceof Map) {
return _.isMapEqual(left, right as Map<any, any>)
}
if (left instanceof Set) {
return _.isSetEqual(left, right as Set<any>)
}
return null
}

const a = new Map([[1, ['a', 'b']]])
const b = new Map([[1, ['a', 'b']]])

_.isEqual(a, b, compareCollections) // => true
```

### Caveats

- Sparse arrays are not supported.
- Cyclical structures are not supported.
- Differences in prototypes (including values from different realms) make objects unequal unless handled by a custom comparer.

For dedicated helpers, see [`isMapEqual`](./isMapEqual) and [`isSetEqual`](./isSetEqual).
77 changes: 53 additions & 24 deletions src/typed/isEqual.ts
Original file line number Diff line number Diff line change
@@ -1,48 +1,77 @@
/**
* Return true if the given values are equal.
* Return true if the given values are deeply equal.
*
* To determine equality, `Object.is()` is used first. If it returns
* false, we do the following special checks:
* - `Date` and `Date` with the same time
* - `RegExp` and `RegExp` with the same pattern/flags
* - object with the same keys and values (recursive)
* - arrays with the same length and elements (recursive)
* - objects with the same keys and values (recursive)
*
* You may pass a custom compare function to handle specific cases.
* Your compare function is called before the final object comparison,
* after all other checks. It can return `null` to default to the
* built-in behavior.
*
* See the documentation for caveats.
*
* @see https://radashi.js.org/reference/typed/isEqual
* @example
* ```ts
* isEqual(0, 0) // => true
* isEqual(0, 1) // => false
* ```
* @version 12.1.0
*/
export function isEqual<TType>(x: TType, y: TType): boolean {
export function isEqual<T>(
x: T,
y: T,
customCompare?: (x: any, y: any) => boolean | null | undefined,
): boolean
export function isEqual(
x: any,
y: any,
customCompare?: (x: any, y: any) => boolean | null | undefined,
): boolean {
if (Object.is(x, y)) {
return true
}
if (x instanceof Date && y instanceof Date) {
return x.getTime() === y.getTime()
}
if (x instanceof RegExp && y instanceof RegExp) {
return x.toString() === y.toString()
}
if (
!x ||
!y ||
typeof x !== 'object' ||
x === null ||
typeof y !== 'object' ||
y === null
Object.getPrototypeOf(x) !== Object.getPrototypeOf(y)
) {
return false
}
const keysX = Reflect.ownKeys(x as unknown as object) as (keyof typeof x)[]
const keysY = Reflect.ownKeys(y as unknown as object)
if (keysX.length !== keysY.length) {
switch (x.constructor) {
case Object:
break
// Fast path for arrays
case Array:
return (
x.length === y.length &&
(x as any[]).every((item, index) => {
return isEqual(item, y[index])
})
)
case Date:
return x.getTime() === y.getTime()
case RegExp:
return x.toString() === y.toString()
default: {
const result = customCompare?.(x, y)
if (result != null) {
return result
}
}
}
const kx = Reflect.ownKeys(x)
const ky = Reflect.ownKeys(y)
if (kx.length !== ky.length) {
return false
}
for (let i = 0; i < keysX.length; i++) {
if (!Reflect.has(y as unknown as object, keysX[i])) {
return false
}
if (!isEqual(x[keysX[i]], y[keysX[i]])) {
for (const key of kx as (keyof typeof x)[]) {
if (
!Object.prototype.hasOwnProperty.call(y, key) ||
!isEqual(x[key], y[key])
) {
return false
}
}
Expand Down
Loading
Loading