Skip to content

Traits & Behaviours

JavaScript’s built-in operators answer surprisingly few questions about your data. === compares objects by reference, not by content. < only works for a handful of primitives. There is no standard way to ask “do these two values mean the same thing?” or “which of these comes first?” for your own types.

Effect closes that gap with a small set of traits — interfaces and modules that describe behaviours a value can have. The most important ones are:

  • Equal / Hash — value-based equality. Two values are equal when their contents match, not when they share a reference. Hash is the companion that lets hash-based collections (HashMap, HashSet) bucket values quickly.
  • Order — a total ordering. An Order<A> is a comparison function that powers sorting, min/max, clamping, and range checks.
  • Equivalence — a relaxed notion of equality. An Equivalence<A> decides when two values should be treated as equivalent for a particular purpose (case-insensitive strings, dates by timestamp, records by a single field).

These traits are the foundation a lot of the rest of Effect is built on. When you compare two Options, deduplicate an Array, or store domain objects in a HashSet, you are relying on Equal, Order, and Equivalence under the hood.

import { Equal, Order, Array } from "effect"
// Value equality: same contents, different references
console.log(Equal.equals({ id: 1 }, { id: 1 })) // true
console.log({ id: 1 } === { id: 1 }) // false (reference equality)
// A reusable comparator for objects, derived from a primitive Order
const byAge = Order.mapInput(
Order.Number,
(user: { name: string; age: number }) => user.age
)
const users = [
{ name: "Charlie", age: 30 },
{ name: "Bob", age: 25 }
]
console.log(Array.sort(users, byAge).map((u) => u.name))
// ["Bob", "Charlie"]

Equal.equals walks the structure of both values and compares them field by field. Order.mapInput adapts the built-in Order.Number so it can compare whole user objects by their age. Neither requires you to write any comparison logic by hand — you compose existing behaviours instead.

There are two ways a type participates in these behaviours:

  1. The type implements the interface. Classes can implement Equal and Hash directly so that Equal.equals and hash collections understand them. This is how Data classes get structural equality for free.
  2. You supply an instance separately. Order<A> and Equivalence<A> are plain functions you build and pass to APIs like Array.sort or Array.dedupeWith. The value itself does not need to know anything about them.

Equal and Equivalence look similar but serve different roles. Equal is the one canonical structural equality used throughout Effect; Equivalence lets you define many situational notions of “the same” for a single type.

  • Equal & Hash — value equality with Equal, the Hash contract, and how to implement both on your own classes.
  • Order — building total orders, composing them for multi-field sorting, and turning them into predicates and range checks.
  • Equivalence — defining custom equality relations and combining them for structs, tuples, arrays, and records.
  • Data TypesData classes implement Equal/Hash automatically.
  • Schema — derive an Equivalence from a schema with Schema.toEquivalence.