Skip to content

Equal & Hash

In JavaScript, === compares objects by reference: two distinct objects are never equal, even if every field matches. That makes it awkward to ask the question you usually care about — do these two values represent the same thing?

The Equal module answers that question with structural equality: Equal.equals walks both values and compares their contents. It works out of the box for primitives, arrays, plain objects, Map, Set, Date, and RegExp, and any type can opt into custom equality by implementing the Equal interface.

import { Equal } from "effect"
// Reference equality says these are different objects
console.log({ name: "Alice", roles: ["admin"] } === { name: "Alice", roles: ["admin"] })
// false
// Structural equality compares the contents instead
console.log(
Equal.equals(
{ name: "Alice", roles: ["admin"] },
{ name: "Alice", roles: ["admin"] }
)
)
// true
// It works recursively, on Maps/Sets (order-independent), and with NaN
console.log(Equal.equals(new Map([["a", 1]]), new Map([["a", 1]]))) // true
console.log(Equal.equals(NaN, NaN)) // true (unlike ===)

Equal.equals never throws and always returns a boolean. Its curried form is handy for building predicates:

import { Array, Equal } from "effect"
const is42 = Equal.equals(42)
console.log(is42(42)) // true
// Use Equal semantics to deduplicate a collection
const eq = Equal.asEquivalence<number>()
console.log(Array.dedupeWith([1, 2, 2, 3, 1], eq)) // [1, 2, 3]

Structural comparison can be expensive. Before comparing fields, Equal checks a cheap numeric hash of each value: if the hashes differ, the values cannot be equal and the comparison stops early. Hash-based collections like HashMap and HashSet use the same hash to bucket values.

A hash is a fingerprint, not a proof of equality — collisions are possible — so hashing and equality always travel together. This is the Hash contract:

If Equal.equals(a, b) is true, then Hash.hash(a) must equal Hash.hash(b).

The Equal interface extends Hash, so any type with custom equality must also provide a matching hash.

To give your own class value semantics, implement both [Equal.symbol] (equality) and [Hash.symbol] (hashing). Build the hash from the same fields you compare, so the contract holds automatically.

import { Equal, Hash } from "effect"
// A domain identifier that should compare by value, not by reference
class UserId implements Equal.Equal {
constructor(readonly region: string, readonly id: string) {}
// Equal: two UserIds match when both fields match
[Equal.symbol](that: Equal.Equal): boolean {
return (
that instanceof UserId &&
this.region === that.region &&
this.id === that.id
)
}
// Hash: derive from the SAME fields so the contract holds.
// Hash.combine folds the field hashes into one number.
[Hash.symbol](): number {
return Hash.combine(Hash.string(this.region))(Hash.string(this.id))
}
}
const a = new UserId("eu", "user-1")
const b = new UserId("eu", "user-1")
const c = new UserId("us", "user-1")
console.log(Equal.equals(a, b)) // true (same contents)
console.log(Equal.equals(a, c)) // false (different region)
console.log(a === b) // false (still distinct references)

Because a and b are now equal and hash to the same value, they behave as a single key in hash-based collections:

import { HashSet } from "effect"
const ids = HashSet.make(
new UserId("eu", "user-1"),
new UserId("eu", "user-1") // duplicate by value
)
console.log(HashSet.size(ids)) // 1
import { Data, Equal } from "effect"
class Person extends Data.Class<{
readonly name: string
readonly age: number
}> {}
const mike1 = new Person({ name: "Mike", age: 30 })
const mike2 = new Person({ name: "Mike", age: 30 })
console.log(Equal.equals(mike1, mike2)) // true
  • Treat values as immutable after comparing them. Comparison and hash results are cached per object. Mutating an object after its first Equal.equals or Hash.hash call yields stale results.
  • Implementing only one of the pair is unsafe. If you provide [Equal.symbol] but a hash that ignores the same fields, equal values may hash differently and silently disappear from a HashMap. Always derive the hash from the fields you compare.
  • One-sided Equal is never equal. If only one of two operands implements Equal, Equal.equals returns false.
  • Functions and NaN. Functions without an Equal implementation compare by reference; NaN is treated as equal to NaN (unlike ===).

When you need reference identity for a mutable object — so that two snapshots with the same contents are still considered different — opt out explicitly:

import { Equal } from "effect"
const a = { x: 1 }
const b = { x: 1 }
console.log(Equal.equals(a, b)) // true (structural)
const aRef = Equal.byReference(a)
console.log(Equal.equals(aRef, b)) // false (compared by reference)
console.log(aRef.x) // 1 (the proxy reads through to the original)

Every public export of the Equal module. Import the namespace with import { Equal } from "effect".

Checks whether two values are deeply structurally equal. Never throws, always returns a boolean. Supports both the binary form (self, that) and the curried (data-last) form (that) for building reusable predicates.

import { Equal } from "effect"
// Binary form
console.log(Equal.equals(1, 1)) // => true
console.log(Equal.equals({ a: 1 }, { a: 1 })) // => true
console.log(Equal.equals([1, 2], [1, 3])) // => false
// Order-independent for Map / Set
const m1 = new Map([["a", 1], ["b", 2]])
const m2 = new Map([["b", 2], ["a", 1]])
console.log(Equal.equals(m1, m2)) // => true
// Curried form: pass the comparison target first
const isOrigin = Equal.equals({ x: 0, y: 0 })
console.log(isOrigin({ x: 0, y: 0 })) // => true
console.log(isOrigin({ x: 1, y: 0 })) // => false

Type guard that returns true when a value implements the Equal interface (carries an [Equal.symbol] method), narrowing it to Equal.Equal.

import { Equal, Hash } from "effect"
class Token implements Equal.Equal {
constructor(readonly value: string) {}
[Equal.symbol](that: Equal.Equal): boolean {
return that instanceof Token && this.value === that.value
}
[Hash.symbol](): number {
return Hash.string(this.value)
}
}
console.log(Equal.isEqual(new Token("abc"))) // => true
console.log(Equal.isEqual({ x: 1 })) // => false
console.log(Equal.isEqual(42)) // => false

Bridges Equal.equals into an Equivalence<A> — a plain (a, b) => boolean function — for APIs that expect an Equivalence (such as Array.dedupeWith or Equivalence.mapInput).

import { Array, Equal } from "effect"
const eq = Equal.asEquivalence<number>()
console.log(eq(2, 2)) // => true
console.log(Array.dedupeWith([1, 2, 2, 3, 1], eq)) // => [1, 2, 3]

See Equivalence for building richer relations.

Returns a Proxy wrapping obj that opts that handle out of structural equality: Equal.equals returns false for it unless compared with the exact same reference. The original object is left untouched, and property access reads through. Each call creates a new proxy, so byReference(x) !== byReference(x). The original object is not mutated (unlike byReferenceUnsafe).

import { Equal } from "effect"
const a = { x: 1 }
const b = { x: 1 }
console.log(Equal.equals(a, b)) // => true (structural)
const aRef = Equal.byReference(a)
console.log(Equal.equals(aRef, b)) // => false (reference)
console.log(Equal.equals(aRef, aRef)) // => true (same reference)
console.log(aRef.x) // => 1 (proxy reads through)

Marks an object for reference equality in place by registering it in an internal WeakSet, returning the same object (no proxy). Unlike Equal.byReference, this mutates the object’s equality behavior permanently and irreversibly, but avoids proxy overhead — use it on hot paths.

import { Equal } from "effect"
const obj1 = { a: 1, b: 2 }
const obj2 = { a: 1, b: 2 }
Equal.byReferenceUnsafe(obj1)
console.log(Equal.equals(obj1, obj2)) // => false (reference)
console.log(Equal.equals(obj1, obj1)) // => true (same reference)
console.log(obj1 === Equal.byReferenceUnsafe(obj1)) // => true (same object)

Builds an order-independent Map comparator from a key Equivalence and a value Equivalence. The returned function compares two iterables of [key, value] entries: every entry on the left must have a matching entry on the right.

import { Equal } from "effect"
// Compare maps where values match within a tolerance of 1
const compareMaps = Equal.makeCompareMap<string, number>(
(a, b) => a === b,
(a, b) => Math.abs(a - b) <= 1
)
const left = new Map([["a", 10], ["b", 20]])
const right = new Map([["b", 21], ["a", 10]])
console.log(compareMaps(left, right)) // => true (20 ~ 21, order ignored)

Builds an order-independent Set comparator from a single element Equivalence. Every element on the left must have a matching element on the right.

import { Equal } from "effect"
// Case-insensitive set comparison
const compareSets = Equal.makeCompareSet<string>(
(a, b) => a.toLowerCase() === b.toLowerCase()
)
console.log(compareSets(new Set(["A", "B"]), new Set(["b", "a"]))) // => true
console.log(compareSets(new Set(["A"]), new Set(["A", "B"]))) // => false

The string key ("~effect/interfaces/Equal") used as the computed property name for the equality method when implementing Equal.Equal. Prefer Equal.isEqual for detection rather than checking for this key manually.

import { Equal, Hash } from "effect"
class Money implements Equal.Equal {
constructor(readonly cents: number) {}
[Equal.symbol](that: Equal.Equal): boolean {
return that instanceof Money && this.cents === that.cents
}
[Hash.symbol](): number {
return Hash.number(this.cents)
}
}
console.log(typeof Equal.symbol) // => "string"
console.log(Equal.equals(new Money(500), new Money(500))) // => true

The interface for types that define their own equality. It extends Hash.Hash, so implementors must provide both [Equal.symbol](that) and [Hash.symbol](). Equal.equals delegates to the equality method when both operands implement it; if only one does, they are never equal.

import { Equal, Hash } from "effect"
class Coordinate implements Equal.Equal {
constructor(readonly x: number, readonly y: number) {}
[Equal.symbol](that: Equal.Equal): boolean {
return that instanceof Coordinate && this.x === that.x && this.y === that.y
}
[Hash.symbol](): number {
return Hash.string(`${this.x},${this.y}`)
}
}
console.log(Equal.equals(new Coordinate(1, 2), new Coordinate(1, 2))) // => true
console.log(Equal.equals(new Coordinate(1, 2), new Coordinate(3, 4))) // => false

Every public export of the Hash module. Hashes are small numeric fingerprints used to bucket values quickly — not cryptographic digests and not proof of equality. Import the namespace with import { Hash } from "effect".

Computes a hash value for any input, dispatching by JavaScript type (primitives, arrays, typed arrays, Map, Set, plain objects, Date, RegExp, and custom Hash implementors). Structural object hashes are cached after first computation.

import { Hash } from "effect"
console.log(Hash.hash(42)) // => a number
console.log(Hash.hash("hello")) // => a number
console.log(Hash.hash([1, 2, 3]) === Hash.hash([1, 2, 3])) // => true
console.log(Hash.hash({ a: 1 }) === Hash.hash({ a: 1 })) // => true

Folds two hash values into one with (self * 53) ^ b. Dual/curried: useful when composing field hashes inside a custom [Hash.symbol] implementation.

import { Hash, pipe } from "effect"
const h1 = Hash.hash("hello")
const h2 = Hash.hash("world")
// Data-first
console.log(Hash.combine(h1, h2)) // => a combined number
// Data-last (pipeable)
console.log(pipe(h1, Hash.combine(h2))) // => same combined number

Bit-mixes a raw numeric hash to improve distribution and reduce collisions. Used internally by the other hash functions; apply it to the final value of a custom hash built from raw arithmetic.

import { Hash } from "effect"
console.log(Hash.optimize(1234567890)) // => an optimized number
// Commonly wraps a derived hash
console.log(Hash.optimize(Hash.string("hello"))) // => an optimized number

Hashes a JavaScript number, with distinct handling for NaN, Infinity, and -Infinity.

import { Hash } from "effect"
console.log(Hash.number(100) === Hash.number(100)) // => true
// Infinity / -Infinity / NaN are hashed via their string form
console.log(Hash.number(Infinity) === Hash.number(Infinity)) // => true
console.log(Hash.number(NaN) === Hash.number(NaN)) // => true

Hashes a string using a djb2-style algorithm, then optimizes the result.

import { Hash } from "effect"
console.log(Hash.string("test") === Hash.string("test")) // => true
console.log(Hash.string("a") === Hash.string("b")) // => false

Hashes an iterable by XOR-folding the hash of each element. Because it uses XOR, reordered inputs can collide — a hash is not an equality proof.

import { Hash } from "effect"
console.log(Hash.array([1, 2, 3]) === Hash.array([1, 2, 3])) // => true
console.log(Hash.array([1, 2, 3]) === Hash.array([3, 2, 1])) // => true (XOR collision)

Computes a structural hash from all of an object’s keys (including symbol and relevant prototype keys). Objects with the same properties hash equally.

import { Hash } from "effect"
const a = { name: "John", age: 30 }
const b = { name: "John", age: 30 }
const c = { name: "Jane", age: 25 }
console.log(Hash.structure(a) === Hash.structure(b)) // => true
console.log(Hash.structure(a) === Hash.structure(c)) // => false

Like Hash.structure but hashes only a selected set of keys — handy for building a custom hash that ignores incidental fields.

import { Hash } from "effect"
const p1 = { name: "John", age: 30, city: "New York" }
const p2 = { name: "John", age: 30, city: "Boston" }
// Hash ignores `city`
console.log(
Hash.structureKeys(p1, ["name", "age"]) ===
Hash.structureKeys(p2, ["name", "age"])
) // => true

Generates a random, reference-stable hash for an object and caches it in a WeakMap. Use it to hash mutable objects by identity rather than content. Only accepts objects.

import { Hash } from "effect"
const obj1 = { a: 1 }
const obj2 = { a: 1 }
console.log(Hash.random(obj1) === Hash.random(obj1)) // => true (cached)
console.log(Hash.random(obj1) === Hash.random(obj2)) // => false (different objects)

Type guard that returns true when a value implements the Hash interface (carries a [Hash.symbol] method).

import { Hash } from "effect"
class Hashable implements Hash.Hash {
[Hash.symbol]() {
return 42
}
}
console.log(Hash.isHash(new Hashable())) // => true
console.log(Hash.isHash({})) // => false
console.log(Hash.isHash("string")) // => false

The string key ("~effect/interfaces/Hash") used as the computed property name for the hash method when implementing Hash.Hash.

import { Hash } from "effect"
class Id implements Hash.Hash {
constructor(readonly value: string) {}
[Hash.symbol](): number {
return Hash.string(this.value)
}
}
console.log(typeof Hash.symbol) // => "string"
console.log(new Id("x")[Hash.symbol]() === Hash.string("x")) // => true

The interface for types that supply their own stable hash via a [Hash.symbol](): number method. It is the supertype of Equal.Equal; implement it directly when a type needs hashing but not custom equality (e.g. a key for a hash collection).

import { Hash } from "effect"
class CacheKey implements Hash.Hash {
constructor(readonly id: string, readonly region: string) {}
[Hash.symbol](): number {
return Hash.combine(Hash.string(this.region))(Hash.string(this.id))
}
}
console.log(Hash.hash(new CacheKey("user-1", "eu"))) // => a number
  • Equivalence — situational equality relations when one canonical Equal is not enough.
  • Data Types — value classes and tagged unions with built-in equality.
  • State Management and CachingHashMap and HashSet rely on the Equal/Hash contract.