Equivalence
An Equivalence<A> is a function (a: A, b: A) => boolean that decides when two
values should be treated as the same — for some specific purpose. Unlike
Equal, which is the single canonical structural
equality used throughout Effect, you can define many equivalences for one
type: compare strings case-insensitively, dates by their timestamp, or users by
their id alone.
Every equivalence must be reflexive (eq(a, a) is true), symmetric, and
transitive. Many Effect APIs accept an Equivalence — Array.dedupeWith,
Array.containsWith, and others — so a custom equivalence is often all you need
to make a built-in operation behave the way your domain expects.
import { Array, Equivalence } from "effect"
// Two strings are "the same" if they match ignoring caseconst caseInsensitive = Equivalence.make<string>( (a, b) => a.toLowerCase() === b.toLowerCase())
console.log(caseInsensitive("Hello", "HELLO")) // true
// Deduplicate using that notion of samenessconsole.log(Array.dedupeWith(["Hello", "world", "HELLO", "World"], caseInsensitive))// ["Hello", "world"]Equivalence.make adds a fast reference-equality (===) check before calling
your function, so identical references short-circuit to true.
Building blocks: mapInput and combine
Section titled “Building blocks: mapInput and combine”Two combinators do most of the work. mapInput derives an equivalence for a
larger type by projecting out the part you care about; combine ANDs two
equivalences together so both must agree.
import { Equivalence } from "effect"
interface User { readonly id: number readonly name: string readonly email: string}
// Compare users by id only — name and email are ignoredconst byId = Equivalence.mapInput( Equivalence.strictEqual<number>(), (user: User) => user.id)
const a = { id: 1, name: "Alice", email: "alice@example.com" }const b = { id: 1, name: "Alice Smith", email: "alice@work.com" }const c = { id: 2, name: "Bob", email: "bob@example.com" }
console.log(byId(a, b)) // true (same id)console.log(byId(a, c)) // false (different id)combine chains two equivalences; both have to hold. The second is only checked
when the first returns true (short-circuiting):
import { Equivalence } from "effect"
interface User { readonly name: string readonly age: number}
const byName = Equivalence.mapInput( Equivalence.strictEqual<string>(), (u: User) => u.name)const byAge = Equivalence.mapInput( Equivalence.strictEqual<number>(), (u: User) => u.age)
// Equivalent only when BOTH name and age matchconst sameUser = Equivalence.combine(byName, byAge)
console.log(sameUser({ name: "Alice", age: 30 }, { name: "Alice", age: 30 })) // trueconsole.log(sameUser({ name: "Alice", age: 30 }, { name: "Alice", age: 31 })) // falseStructured equivalences
Section titled “Structured equivalences”You rarely build struct equivalences field-by-field with combine — there are
direct combinators. Equivalence.Struct takes a per-field equivalence and only
compares the listed fields (extra fields are ignored). Tuple, Array, and
Record cover the other shapes.
import { Equivalence } from "effect"
interface Person { readonly name: string readonly age: number readonly email: string}
// Names and emails compare case-insensitively; age must match exactlyconst caseInsensitive = Equivalence.mapInput( Equivalence.strictEqual<string>(), (s: string) => s.toLowerCase())
const personEq = Equivalence.Struct({ name: caseInsensitive, age: Equivalence.Number, email: caseInsensitive})
const p1 = { name: "Alice", age: 30, email: "alice@example.com" }const p2 = { name: "ALICE", age: 30, email: "ALICE@EXAMPLE.COM" }
console.log(personEq(p1, p2)) // true (different casing, same person)The other structured combinators follow the same pattern:
import { Equivalence } from "effect"
// Tuple: a different equivalence per positionconst point = Equivalence.Tuple([Equivalence.Number, Equivalence.Number])console.log(point([1, 2], [1, 2])) // true
// Array: one equivalence for every element; lengths must matchconst tags = Equivalence.Array(Equivalence.String)console.log(tags(["a", "b"], ["a", "b"])) // trueconsole.log(tags(["a"], ["a", "b"])) // false (different length)
// Record: compares all keys; both objects need the same key setconst scores = Equivalence.Record(Equivalence.Number)console.log(scores({ alice: 1 }, { alice: 1 })) // trueDeriving an equivalence from a Schema
Section titled “Deriving an equivalence from a Schema”If you already describe your data with Schema, you do not need to
assemble an equivalence by hand — Schema.toEquivalence derives one that matches
the schema’s structure.
import { Schema } from "effect"
const User = Schema.Struct({ id: Schema.Number, name: Schema.String})
const eq = Schema.toEquivalence(User)
console.log(eq({ id: 1, name: "Alice" }, { id: 1, name: "Alice" })) // trueconsole.log(eq({ id: 1, name: "Alice" }, { id: 2, name: "Alice" })) // falseEquivalence vs. Equal
Section titled “Equivalence vs. Equal”Both answer “are these the same?”, but they fill different roles:
Equalis the one structural equality baked into Effect. Types implement it (or inherit it fromData), and hash-based collections rely on it. Use it when you want “same contents” without configuration.Equivalenceis a value you build and pass in to define situational sameness for a specific operation. Use it when “the same” depends on context — ignoring case, comparing by a key, applying a numeric tolerance.
You can bridge from one to the other when needed: Equal.asEquivalence() wraps
the canonical structural equality as an Equivalence for APIs that expect one.
import { Array, Equal } from "effect"
const result = Array.dedupeWith([1, 2, 2, 3], Equal.asEquivalence<number>())console.log(result) // [1, 2, 3]Equivalence reference
Section titled “Equivalence reference”Everything the Equivalence module exports. The combinators and structured
helpers above are repeated here as compact entries so the whole module is
scannable in one place.
Equivalence<A>
Section titled “Equivalence<A>”The core type: a function (self: A, that: A) => boolean that must be
reflexive, symmetric, and transitive. It is contravariant in A, so an
Equivalence<unknown> can be used wherever an Equivalence<string> is expected.
import type { Equivalence } from "effect"
const pointEq: Equivalence.Equivalence<{ x: number; y: number }> = (a, b) => a.x === b.x && a.y === b.y
console.log(pointEq({ x: 1, y: 2 }, { x: 1, y: 2 })) // => trueconsole.log(pointEq({ x: 1, y: 2 }, { x: 9, y: 2 })) // => falseEquivalence.make
Section titled “Equivalence.make”Wraps a predicate as an Equivalence, adding a === fast path: identical
references return true without calling your function.
import { Equivalence } from "effect"
const tolerance = Equivalence.make<number>((a, b) => Math.abs(a - b) < 0.0001)
console.log(tolerance(1.0, 1.00001)) // => trueconsole.log(tolerance(1.0, 1.001)) // => falseEquivalence.strictEqual
Section titled “Equivalence.strictEqual”Builds an equivalence backed by raw ===. Good for primitives and reference
equality; note NaN is never equal to itself and objects compare by reference.
import { Equivalence } from "effect"
const eq = Equivalence.strictEqual<number>()
console.log(eq(1, 1)) // => trueconsole.log(eq(NaN, NaN)) // => false (NaN !== NaN)Equivalence.String
Section titled “Equivalence.String”Built-in instance for strings using ===.
import { Equivalence } from "effect"
console.log(Equivalence.String("hello", "hello")) // => trueconsole.log(Equivalence.String("hello", "world")) // => falseEquivalence.Number
Section titled “Equivalence.Number”Built-in instance for numbers. Unlike ===, it treats NaN as equal to NaN.
import { Equivalence } from "effect"
console.log(Equivalence.Number(1, 1)) // => trueconsole.log(Equivalence.Number(NaN, NaN)) // => trueEquivalence.Boolean
Section titled “Equivalence.Boolean”Built-in instance for booleans using ===.
import { Equivalence } from "effect"
console.log(Equivalence.Boolean(true, true)) // => trueconsole.log(Equivalence.Boolean(true, false)) // => falseEquivalence.BigInt
Section titled “Equivalence.BigInt”Built-in instance for bigints using ===.
import { Equivalence } from "effect"
console.log(Equivalence.BigInt(1n, 1n)) // => trueconsole.log(Equivalence.BigInt(1n, 2n)) // => falseEquivalence.Date
Section titled “Equivalence.Date”Built-in instance for Date values. Compares by getTime() via
Equivalence.Number, so distinct instances with the same timestamp are equal,
and two invalid dates (both NaN time) are equal too.
import { Equivalence } from "effect"
const a = new Date("2020-01-01T00:00:00.000Z")const b = new Date("2020-01-01T00:00:00.000Z")
console.log(a === b) // => false (different references)console.log(Equivalence.Date(a, b)) // => true (same time value)console.log(Equivalence.Date(new Date("foo"), new Date("bar"))) // => true (both invalid)Equivalence.combine
Section titled “Equivalence.combine”ANDs two equivalences. The second is only evaluated when the first returns
true (short-circuiting).
import { Equivalence } from "effect"
const byLength = Equivalence.mapInput(Equivalence.Number, (s: string) => s.length)const sameFirst = Equivalence.mapInput(Equivalence.String, (s: string) => s[0])
const eq = Equivalence.combine(byLength, sameFirst)
console.log(eq("cat", "cup")) // => true (len 3, first "c")console.log(eq("cat", "dog")) // => false (different first char)Equivalence.combineAll
Section titled “Equivalence.combineAll”ANDs an iterable of equivalences. An empty iterable yields an equivalence that
always returns true.
import { Equivalence } from "effect"
const eq = Equivalence.combineAll([Equivalence.Number, Equivalence.Number])console.log(eq(1, 1)) // => true
const always = Equivalence.combineAll<number>([])console.log(always(1, 2)) // => true (empty collection)Equivalence.mapInput
Section titled “Equivalence.mapInput”Derives an equivalence for a new type by projecting each input before comparing.
import { Equivalence } from "effect"
const caseInsensitive = Equivalence.mapInput( Equivalence.String, (s: string) => s.toLowerCase())
console.log(caseInsensitive("Hello", "HELLO")) // => trueconsole.log(caseInsensitive("Hello", "World")) // => falseEquivalence.Tuple
Section titled “Equivalence.Tuple”Builds an equivalence for fixed-length tuples, applying one equivalence per position. Tuples of different lengths are never equivalent.
import { Equivalence } from "effect"
const eq = Equivalence.Tuple([Equivalence.String, Equivalence.Number])
console.log(eq(["a", 1], ["a", 1])) // => trueconsole.log(eq(["a", 1], ["a", 2])) // => falseEquivalence.Struct
Section titled “Equivalence.Struct”Builds an equivalence for objects from a per-field equivalence. Only the listed fields are compared; extra properties are ignored. Symbol keys are supported.
import { Equivalence } from "effect"
const eq = Equivalence.Struct({ name: Equivalence.String, age: Equivalence.Number})
console.log(eq({ name: "Alice", age: 30, extra: 1 }, { name: "Alice", age: 30, extra: 2 })) // => trueconsole.log(eq({ name: "Alice", age: 30 }, { name: "Alice", age: 31 })) // => falseEquivalence.Record
Section titled “Equivalence.Record”Builds an equivalence for records/dictionaries, comparing every value with the same equivalence. Both objects must have the exact same set of keys.
import { Equivalence } from "effect"
const eq = Equivalence.Record(Equivalence.Number)
console.log(eq({ a: 1, b: 2 }, { a: 1, b: 2 })) // => trueconsole.log(eq({ a: 1, b: 2 }, { a: 1 })) // => false (different keys)Equivalence.Array
Section titled “Equivalence.Array”Builds an equivalence for arrays, comparing elements positionally with a single equivalence. Lengths must match; empty arrays are equivalent.
import { Equivalence } from "effect"
const eq = Equivalence.Array(Equivalence.Number)
console.log(eq([1, 2, 3], [1, 2, 3])) // => trueconsole.log(eq([1, 2], [1, 2, 3])) // => false (different length)console.log(eq([], [])) // => trueEquivalence.makeReducer
Section titled “Equivalence.makeReducer”Creates a Reducer of Equivalence<A> whose combine/combineAll AND
equivalences together, with an always-true identity. Useful when folding a
collection of equivalences.
import { Equivalence } from "effect"
const reducer = Equivalence.makeReducer<number>()
const combined = reducer.combineAll([ Equivalence.Number, Equivalence.make<number>((a, b) => Math.abs(a - b) < 1)])
console.log(combined(1, 1)) // => trueconsole.log(combined(1, 1.5)) // => false (strict number check fails)console.log(reducer.initialValue(1, 2)) // => true (identity is always-true)Equivalence.EquivalenceTypeLambda
Section titled “Equivalence.EquivalenceTypeLambda”The higher-kinded type lambda for Equivalence, used by Effect’s HKT machinery.
You will rarely reference it directly in application code.
See also
Section titled “See also”- Equal & Hash — the canonical structural equality and its hashing contract.
- Order — when you need an ordering, not just equality.
- Schema — derive equivalences (and more) from your data definitions.