Skip to content

Order

An Order<A> is a comparison function: given two values it returns -1 (the first comes before the second), 0 (they are equal for this ordering), or 1 (the first comes after the second). That single behaviour powers sorting, min/max, clamping, range checks, and ordered data structures.

The module ships ready-made orders for primitives and combinators to build new ones from existing ones — so you almost never write a raw comparison by hand.

import { Order } from "effect"
// Built-in orders for primitives
console.log(Order.Number(1, 2)) // -1 (1 comes before 2)
console.log(Order.String("b", "a")) // 1 (b comes after a)
console.log(Order.Number(1, 1)) // 0 (equal)

You rarely have a bare number to sort. The key combinator is mapInput: it adapts an existing order to a larger type by extracting the value to compare.

import { Array, Order } from "effect"
interface User {
readonly name: string
readonly age: number
}
// Reuse Order.Number, but compare Users by their age
const byAge = Order.mapInput(Order.Number, (user: User) => user.age)
const users: ReadonlyArray<User> = [
{ name: "Charlie", age: 30 },
{ name: "Bob", age: 25 },
{ name: "Alice", age: 30 }
]
console.log(Array.sort(users, byAge).map((u) => u.name))
// ["Bob", "Charlie", "Alice"]

Sorting by age alone leaves the two 30-year-olds in arbitrary order. combine chains orders: it uses the first, and only falls back to the second when the first reports a tie. This is exactly lexicographic, “sort by A then by B” ordering.

import { Array, Order } from "effect"
interface User {
readonly name: string
readonly age: number
}
const byAge = Order.mapInput(Order.Number, (u: User) => u.age)
const byName = Order.mapInput(Order.String, (u: User) => u.name)
// Compare by age first; break ties by name
const byAgeThenName = Order.combine(byAge, byName)
const users: ReadonlyArray<User> = [
{ name: "Charlie", age: 30 },
{ name: "Bob", age: 25 },
{ name: "Alice", age: 30 }
]
console.log(Array.sort(users, byAgeThenName).map((u) => u.name))
// ["Bob", "Alice", "Charlie"] (age ascending, then name within age 30)

For a fixed struct you can express the same thing declaratively with Order.Struct, which compares fields in the order they appear:

import { Order } from "effect"
const byAgeThenName = Order.Struct({
age: Order.Number, // highest-priority field first
name: Order.String
})

flip reverses an order, turning ascending into descending without touching the original:

import { Array, Order } from "effect"
const byAge = Order.mapInput(Order.Number, (u: { age: number }) => u.age)
const byAgeDesc = Order.flip(byAge)
const people = [{ age: 20 }, { age: 40 }, { age: 30 }]
console.log(Array.sort(people, byAgeDesc).map((p) => p.age))
// [40, 30, 20]

An Order is more than a sort key. The module turns it into boolean predicates and selection helpers, all of which take the order as their first argument:

import { Order } from "effect"
const lt = Order.isLessThan(Order.Number)
console.log(lt(1, 2)) // true
// min / max return the first argument when values compare equal
console.log(Order.min(Order.Number)(3, 7)) // 3
console.log(Order.max(Order.Number)(3, 7)) // 7
// Clamp a value into a range and test membership
const clamp = Order.clamp(Order.Number)({ minimum: 1, maximum: 5 })
console.log(clamp(0)) // 1
console.log(clamp(9)) // 5
const inRange = Order.isBetween(Order.Number)
console.log(inRange(3, { minimum: 1, maximum: 5 })) // true

There are matching predicates for every direction: isLessThan, isLessThanOrEqualTo, isGreaterThan, and isGreaterThanOrEqualTo.

Order.Tuple compares fixed-length, heterogeneous positions; Order.Array compares same-typed arrays element by element, with shorter arrays sorting first when one is a prefix of the other.

import { Order } from "effect"
const byTuple = Order.Tuple([Order.Number, Order.String])
console.log(byTuple([1, "b"], [1, "a"])) // 1 (numbers tie, "b" > "a")
const byArray = Order.Array(Order.Number)
console.log(byArray([1, 2], [1, 2, 3])) // -1 (prefix is "less")

When no combinator fits, Order.make wraps a raw comparison. It must return -1, 0, or 1, and it should satisfy the ordering laws — totality, antisymmetry, and transitivity — or sorting and range checks may misbehave.

import { Order } from "effect"
// Order strings by length, longest first
const byLengthDesc = Order.make<string>((a, b) => {
if (a.length > b.length) return -1
if (a.length < b.length) return 1
return 0
})
console.log(byLengthDesc("aaa", "b")) // -1
  • Equivalence — when you need equality but not a full less-than/greater-than ordering.
  • Data Types — collections and values you will frequently sort.
  • Schema — order-based refinements (greaterThan, between) build on Order instances.