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 objectsconsole.log({ name: "Alice", roles: ["admin"] } === { name: "Alice", roles: ["admin"] })// false
// Structural equality compares the contents insteadconsole.log( Equal.equals( { name: "Alice", roles: ["admin"] }, { name: "Alice", roles: ["admin"] } ))// true
// It works recursively, on Maps/Sets (order-independent), and with NaNconsole.log(Equal.equals(new Map([["a", 1]]), new Map([["a", 1]]))) // trueconsole.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 collectionconst eq = Equal.asEquivalence<number>()console.log(Array.dedupeWith([1, 2, 2, 3, 1], eq)) // [1, 2, 3]The Hash companion
Section titled “The Hash companion”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)istrue, thenHash.hash(a)must equalHash.hash(b).
The Equal interface extends Hash, so any type with custom equality must
also provide a matching hash.
Custom equality on a class
Section titled “Custom equality on a class”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 referenceclass 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)) // 1Data classes get this for free
Section titled “Data classes get this for free”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)) // trueGotchas
Section titled “Gotchas”- Treat values as immutable after comparing them. Comparison and hash
results are cached per object. Mutating an object after its first
Equal.equalsorHash.hashcall 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 aHashMap. Always derive the hash from the fields you compare. - One-sided
Equalis never equal. If only one of two operands implementsEqual,Equal.equalsreturnsfalse. - Functions and
NaN. Functions without anEqualimplementation compare by reference;NaNis treated as equal toNaN(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)Equal reference
Section titled “Equal reference”Every public export of the Equal module. Import the namespace with
import { Equal } from "effect".
Equal.equals
Section titled “Equal.equals”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 formconsole.log(Equal.equals(1, 1)) // => trueconsole.log(Equal.equals({ a: 1 }, { a: 1 })) // => trueconsole.log(Equal.equals([1, 2], [1, 3])) // => false
// Order-independent for Map / Setconst 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 firstconst isOrigin = Equal.equals({ x: 0, y: 0 })console.log(isOrigin({ x: 0, y: 0 })) // => trueconsole.log(isOrigin({ x: 1, y: 0 })) // => falseEqual.isEqual
Section titled “Equal.isEqual”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"))) // => trueconsole.log(Equal.isEqual({ x: 1 })) // => falseconsole.log(Equal.isEqual(42)) // => falseEqual.asEquivalence
Section titled “Equal.asEquivalence”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)) // => trueconsole.log(Array.dedupeWith([1, 2, 2, 3, 1], eq)) // => [1, 2, 3]See Equivalence for building richer relations.
Equal.byReference
Section titled “Equal.byReference”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)Equal.byReferenceUnsafe
Section titled “Equal.byReferenceUnsafe”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)Equal.makeCompareMap
Section titled “Equal.makeCompareMap”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 1const 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)Equal.makeCompareSet
Section titled “Equal.makeCompareSet”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 comparisonconst compareSets = Equal.makeCompareSet<string>( (a, b) => a.toLowerCase() === b.toLowerCase())
console.log(compareSets(new Set(["A", "B"]), new Set(["b", "a"]))) // => trueconsole.log(compareSets(new Set(["A"]), new Set(["A", "B"]))) // => falseEqual.symbol
Section titled “Equal.symbol”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))) // => trueEqual.Equal (interface)
Section titled “Equal.Equal (interface)”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))) // => trueconsole.log(Equal.equals(new Coordinate(1, 2), new Coordinate(3, 4))) // => falseHash reference
Section titled “Hash reference”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".
Hash.hash
Section titled “Hash.hash”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 numberconsole.log(Hash.hash("hello")) // => a numberconsole.log(Hash.hash([1, 2, 3]) === Hash.hash([1, 2, 3])) // => trueconsole.log(Hash.hash({ a: 1 }) === Hash.hash({ a: 1 })) // => trueHash.combine
Section titled “Hash.combine”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-firstconsole.log(Hash.combine(h1, h2)) // => a combined number// Data-last (pipeable)console.log(pipe(h1, Hash.combine(h2))) // => same combined numberHash.optimize
Section titled “Hash.optimize”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 hashconsole.log(Hash.optimize(Hash.string("hello"))) // => an optimized numberHash.number
Section titled “Hash.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 formconsole.log(Hash.number(Infinity) === Hash.number(Infinity)) // => trueconsole.log(Hash.number(NaN) === Hash.number(NaN)) // => trueHash.string
Section titled “Hash.string”Hashes a string using a djb2-style algorithm, then optimizes the result.
import { Hash } from "effect"
console.log(Hash.string("test") === Hash.string("test")) // => trueconsole.log(Hash.string("a") === Hash.string("b")) // => falseHash.array
Section titled “Hash.array”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])) // => trueconsole.log(Hash.array([1, 2, 3]) === Hash.array([3, 2, 1])) // => true (XOR collision)Hash.structure
Section titled “Hash.structure”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)) // => trueconsole.log(Hash.structure(a) === Hash.structure(c)) // => falseHash.structureKeys
Section titled “Hash.structureKeys”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"])) // => trueHash.random
Section titled “Hash.random”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)Hash.isHash
Section titled “Hash.isHash”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())) // => trueconsole.log(Hash.isHash({})) // => falseconsole.log(Hash.isHash("string")) // => falseHash.symbol
Section titled “Hash.symbol”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")) // => trueHash.Hash (interface)
Section titled “Hash.Hash (interface)”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 numberSee also
Section titled “See also”- Equivalence — situational equality relations when one
canonical
Equalis not enough. - Data Types — value classes and tagged unions with built-in equality.
- State Management and Caching —
HashMapandHashSetrely on theEqual/Hashcontract.