Skip to content

Record, Struct & Tuple

Three small, complementary modules cover the everyday shapes of plain JavaScript data. They all share the same design: immutable (every operation returns a new value, the input is never mutated) and dual (call data-first as Record.map(record, f) or data-last in a pipeline as pipe(record, Record.map(f))).

ModuleUse it forExample shape
RecordHomogeneous string/symbol-keyed maps — collect/map/filter over entriesRecord<string, number>
StructObjects with a known, heterogeneous shape — pick/omit/evolve while preserving precise types{ name: string; age: number }
TupleFixed-length positional data, each slot a different typereadonly [number, number, string]

Reach for Record when keys are dynamic and values share one type. Reach for Struct when the object has a fixed set of named fields you want to manipulate without losing literal field types. Reach for Tuple when position matters and the length is fixed.

Record treats a plain object as an immutable dictionary. Lookups that might miss return an Option; transformations allocate a new object.

import { Record } from "effect"
const scores = { alice: 1, bob: 2 }
const next = Record.set(scores, "carol", 3)
const doubled = Record.map(next, (score) => score * 2)
console.log(scores) // => { alice: 1, bob: 2 } (unchanged)
console.log(doubled) // => { alice: 2, bob: 4, carol: 6 }
console.log(Record.get(doubled, "alice")) // => Option.some(2)
console.log(Record.get(doubled, "dave")) // => Option.none()

Struct operates on objects with a fixed shape and tracks the precise type through each step: pick narrows to a literal subset, evolve may change a field’s type, renameKeys updates the key names — all reflected in the result type.

import { pipe, Struct } from "effect"
const user = { firstName: "Alice", lastName: "Smith", age: 30, admin: false }
const result = pipe(
user,
Struct.pick(["firstName", "age"]), // { firstName: string; age: number }
Struct.evolve({ age: (n) => n + 1 }), // age stays number
Struct.renameKeys({ firstName: "name" }) // { name: string; age: number }
)
console.log(result) // => { name: "Alice", age: 31 }

Tuple works on fixed-length arrays where each position has its own type. The type system tracks the type at each index, and growth operations like appendElement widen the tuple type.

import { pipe, Tuple } from "effect"
const point = Tuple.make(10, 20) // [number, number]
const labeled = pipe(
point,
Tuple.appendElement("red") // [number, number, string]
)
console.log(labeled) // => [10, 20, "red"]
console.log(Tuple.get(labeled, 2)) // => "red"

ReadonlyRecord<K, A> is a plain object whose keys are known by type and whose values share a common type A. Traversal APIs (map, keys, values, …) use Object.keys, so they visit enumerable string keys only; targeted APIs (has, get, set, remove) also accept symbol keys.

import { Record } from "effect"

The foundational type for immutable string/symbol-keyed mappings.

import type { Record } from "effect"
type UserRecord = Record.ReadonlyRecord<"name" | "age", string | number>
// { readonly name: string | number; readonly age: string | number }

The ReadonlyRecord namespace also exposes type-level helpers: ReadonlyRecord.NonLiteralKey<K> (widens literal keys to string/symbol) and ReadonlyRecord.IntersectKeys<K1, K2> (computes overlapping keys, used by intersection).

import type { Record } from "effect"
type K = Record.ReadonlyRecord.NonLiteralKey<"foo" | "bar"> // string
type I = Record.ReadonlyRecord.IntersectKeys<"a" | "b", "b" | "c"> // "b"

The higher-kinded type lambda for records, used to plug records into generic type-constructor machinery (HKT.Kind).

import type { HKT, Record } from "effect"
type Settings = HKT.Kind<
Record.ReadonlyRecordTypeLambda<"port" | "retries">,
never,
never,
never,
number
> // Record<"port" | "retries", number>

Creates a new empty record, typed for future operations.

const r = Record.empty<string, number>()
console.log(r) // => {}

Creates a record from a single key/value pair.

console.log(Record.singleton("a", 1)) // => { a: 1 }

Builds a record from an iterable, mapping each element to a [key, value] tuple.

console.log(Record.fromIterableWith([1, 2, 3], (a) => [String(a), a * 2]))
// => { "1": 2, "2": 4, "3": 6 }

Builds a record from an iterable, deriving each key with the provided function and storing the element as the value.

const users = [
{ id: "2", name: "name2" },
{ id: "1", name: "name1" }
]
console.log(Record.fromIterableBy(users, (u) => u.id))
// => { "2": { id: "2", name: "name2" }, "1": { id: "1", name: "name1" } }

Builds a record from an iterable of [key, value] pairs (the inverse of toEntries). Later duplicate keys overwrite earlier ones.

console.log(Record.fromEntries([["a", 1], ["b", 2]])) // => { a: 1, b: 2 }

Transforms record entries into an array using a (key, value) mapping function.

console.log(Record.collect({ a: 1, b: 2, c: 3 }, (key, n) => [key, n]))
// => [["a", 1], ["b", 2], ["c", 3]]

Returns the record’s entries as an array of [key, value] tuples.

console.log(Record.toEntries({ a: 1, b: 2, c: 3 }))
// => [["a", 1], ["b", 2], ["c", 3]]

Returns the record’s string keys as an array.

console.log(Record.keys({ a: 1, b: 2, c: 3 })) // => ["a", "b", "c"]

Returns the record’s values as an array.

console.log(Record.values({ a: 1, b: 2, c: 3 })) // => [1, 2, 3]

Returns the number of string-keyed entries.

console.log(Record.size({ a: "a", b: 1, c: true })) // => 3

Checks whether a key exists (works with symbol keys too).

console.log(Record.has({ a: 1, b: 2 }, "a")) // => true
console.log(Record.has({ a: 1, b: 2 }, "c")) // => false

Retrieves a value safely as an Option.

import { Record } from "effect"
const person: Record<string, unknown> = { name: "John Doe", age: 35 }
console.log(Record.get(person, "name")) // => Option.some("John Doe")
console.log(Record.get(person, "email")) // => Option.none()

Type guard: true when a mutable record has no keys.

console.log(Record.isEmptyRecord({})) // => true
console.log(Record.isEmptyRecord({ a: 3 })) // => false

The same guard, typed for ReadonlyRecord.

console.log(Record.isEmptyReadonlyRecord({})) // => true
console.log(Record.isEmptyReadonlyRecord({ a: 3 })) // => false

Returns the first [key, value] entry satisfying a predicate, as an Option.

console.log(
Record.findFirst({ a: 1, b: 2, c: 3 }, (value, key) => value > 1 && key !== "b")
)
// => Option.some(["c", 3])

Checks whether every entry satisfies the predicate (also acts as a type guard when given a refinement).

console.log(Record.every({ a: 1, b: 2 }, (n) => n > 0)) // => true
console.log(Record.every({ a: 1, b: -1 }, (n) => n > 0)) // => false

Checks whether at least one entry satisfies the predicate.

console.log(Record.some({ a: 1, b: 2 }, (n) => n > 1)) // => true
console.log(Record.some({ a: 1, b: 2 }, (n) => n > 2)) // => false

All modifying APIs return new records. The ones that might miss a key return an Option.

Adds or updates a key, returning a new record.

console.log(Record.set({ a: 1, b: 2 }, "a", 5)) // => { a: 5, b: 2 }
console.log(Record.set({ a: 1, b: 2 }, "c", 5)) // => { a: 1, b: 2, c: 5 }

Applies a function to the value at a key, or returns Option.none() if absent.

const input: Record<string, number> = { a: 3 }
console.log(Record.modify(input, "a", (x) => x * 2)) // => Option.some({ a: 6 })
console.log(Record.modify(input, "b", (x) => x * 2)) // => Option.none()

Replaces the value at an existing key; Option.none() if the key is absent.

console.log(Record.replace({ a: 1, b: 2 }, "a", 10)) // => Option.some({ a: 10, b: 2 })
console.log(Record.replace(Record.empty<string>(), "a", 10)) // => Option.none()

Returns a shallow copy without the given key.

console.log(Record.remove({ a: 1, b: 2 }, "a")) // => { b: 2 }

Removes a key and returns Option<[value, restOfRecord]>.

const input: Record<string, number> = { a: 1, b: 2 }
console.log(Record.pop(input, "a")) // => Option.some([1, { b: 2 }])
console.log(Record.pop(input, "c")) // => Option.none()

Maps over the values (the callback also receives the key).

console.log(Record.map({ a: 3, b: 5 }, (n) => `-${n}`)) // => { a: "-3", b: "-5" }
console.log(Record.map({ a: 3, b: 5 }, (n, key) => `${key}-${n}`))
// => { a: "a-3", b: "b-5" }

Maps over the keys, preserving values.

console.log(Record.mapKeys({ a: 3, b: 5 }, (key) => key.toUpperCase()))
// => { A: 3, B: 5 }

Maps both keys and values at once, returning a new [key, value] per entry.

console.log(Record.mapEntries({ a: 3, b: 5 }, (a, key) => [key.toUpperCase(), a + 1]))
// => { A: 4, B: 6 }

Maps and filters in one pass using Result: Result.succeed keeps the value, Result.failVoid drops the entry.

import { Record, Result } from "effect"
const f = (a: number) => (a > 2 ? Result.succeed(a * 2) : Result.failVoid)
console.log(Record.filterMap({ a: 1, b: 2, c: 3 }, f)) // => { c: 6 }

Keeps entries whose value matches the predicate (or refinement).

console.log(Record.filter({ a: 1, b: 2, c: 3, d: 4 }, (n) => n > 2)) // => { c: 3, d: 4 }

Splits entries into [failures, successes] by applying a Result-returning function.

import { Record, Result } from "effect"
const f = (n: number) => (n % 2 === 0 ? Result.succeed(n) : Result.fail(n))
console.log(Record.partition({ a: 1, b: 2, c: 3 }, f)) // => [{ a: 1, c: 3 }, { b: 2 }]

Splits a record of Result values into [failures, successes].

import { Record, Result } from "effect"
console.log(Record.separate({ a: Result.fail("e"), b: Result.succeed(1) }))
// => [{ a: "e" }, { b: 1 }]

Keeps only the Some values from a record of Options.

import { Option, Record } from "effect"
console.log(Record.getSomes({ a: Option.some(1), b: Option.none(), c: Option.some(2) }))
// => { a: 1, c: 2 }

Keeps only the failures from a record of Results.

import { Record, Result } from "effect"
console.log(
Record.getFailures({ a: Result.succeed(1), b: Result.fail("err"), c: Result.succeed(2) })
)
// => { b: "err" }

Keeps only the successes from a record of Results.

import { Record, Result } from "effect"
console.log(
Record.getSuccesses({ a: Result.succeed(1), b: Result.fail("err"), c: Result.succeed(2) })
)
// => { a: 1, c: 2 }

Folds the entries into a single accumulated value.

console.log(Record.reduce({ a: 1, b: 2, c: 3 }, 0, (acc, value) => acc + value)) // => 6

Merges two records, keeping keys from both; overlapping keys are merged with the combine function.

console.log(Record.union({ a: 1, b: 2 }, { b: 3, c: 4 }, (a, b) => a + b))
// => { a: 1, b: 5, c: 4 }

Keeps only keys present in both records, merging their values.

console.log(Record.intersection({ a: 1, b: 2 }, { b: 3, c: 4 }, (a, b) => a + b))
// => { b: 5 }

Keeps only keys unique to each record; shared keys are dropped.

console.log(Record.difference({ a: 1, b: 2 }, { b: 3, c: 4 })) // => { a: 1, c: 4 }

Checks whether every key/value of self appears in that, comparing values with Effect equality.

console.log(Record.isSubrecord({ a: 1 } as Record<string, number>, { a: 1, b: 2 })) // => true
console.log(Record.isSubrecord({ a: 1, b: 2 }, { a: 1 } as Record<string, number>)) // => false

Like isSubrecord, but uses a supplied Equivalence to compare values.

import { Equivalence, Record } from "effect"
const isSub = Record.isSubrecordBy(
Equivalence.make<string>((a, b) => a.toLowerCase() === b.toLowerCase())
)
console.log(isSub({ role: "Admin" }, { role: "admin", status: "active" })) // => true

Builds an Equivalence for records from an Equivalence for values. Two records are equivalent when they have the same keys and equivalent values.

import { Equal, Record } from "effect"
const eq = Record.makeEquivalence(Equal.asEquivalence<number>())
console.log(eq({ a: 1, b: 2 }, { a: 1, b: 2 })) // => true
console.log(eq({ a: 1, b: 2 }, { a: 1, b: 3 })) // => false

Builds a Reducer that folds many records into one with union semantics, combining overlapping values with the given Combiner.

import { Number as Num, Record } from "effect"
const reducer = Record.makeReducerUnion<string, number>(Num.ReducerSum)
console.log(reducer.combine({ a: 1, b: 2 }, { b: 3, c: 4 })) // => { a: 1, b: 5, c: 4 }

Builds a Reducer whose combine intersects two records and combines shared values. Note: because the reducer’s initialValue is {}, the default combineAll folds from an empty record and yields {} for ordinary inputs — use combine directly for pairs.

import { Number as Num, Record } from "effect"
const reducer = Record.makeReducerIntersection<string, number>(Num.ReducerSum)
console.log(reducer.combine({ a: 1, b: 2 }, { b: 3, c: 4 })) // => { b: 5 }

Struct manipulates objects with a fixed, heterogeneous shape, preserving exact field types through every operation. Functions iterate with for...in, so inherited enumerable properties are included; keys returns string keys only.

import { Struct } from "effect"

Flattens an intersection like A & B into a single readable object type. Purely cosmetic; preserves readonly.

import type { Struct } from "effect"
type Simplified = Struct.Simplify<{ a: string } & { b: number }>
// { a: string; b: number }

Strips readonly modifiers (and flattens, like Simplify).

import type { Struct } from "effect"
type Writable = Struct.Mutable<{ readonly a: string; readonly b: number }>
// { a: string; b: number }

The type-level equivalent of { ...T, ...U }: merges two object types with the right side winning on overlapping keys.

import type { Struct } from "effect"
type Merged = Struct.Assign<{ a: string; b: number }, { b: boolean; c: string }>
// { a: string; b: boolean; c: string }

Extracts a single property; the return type is narrowed to S[K].

import { pipe, Struct } from "effect"
console.log(pipe({ name: "Alice", age: 30 }, Struct.get("name"))) // => "Alice"

Returns the string keys of a struct, typed as Array<keyof S & string>. Symbol keys are excluded.

import { Struct } from "effect"
const user = { name: "Alice", age: 30, [Symbol.for("id")]: 1 }
console.log(Struct.keys(user)) // => ["name", "age"]

Creates a new struct with only the named keys — and preserves the literal field types of those keys.

import { pipe, Struct } from "effect"
const user = { name: "Alice", age: 30, admin: true }
const result = pipe(user, Struct.pick(["name", "age"]))
// result: { name: string; age: number }
console.log(result) // => { name: "Alice", age: 30 }

The inverse of pick: a new struct with the named keys removed, types preserved.

import { pipe, Struct } from "effect"
const user = { name: "Alice", age: 30, password: "secret" }
console.log(pipe(user, Struct.omit(["password"]))) // => { name: "Alice", age: 30 }

Runtime { ...self, ...that } with proper Assign typing — that wins on overlapping keys.

import { pipe, Struct } from "effect"
const defaults = { theme: "light", lang: "en" }
const overrides = { theme: "dark", fontSize: 14 }
console.log(pipe(defaults, Struct.assign(overrides)))
// => { theme: "dark", lang: "en", fontSize: 14 }

Transforms selected values with per-key functions; untouched keys are copied. Each function may change the field’s type, and the result type tracks that.

import { pipe, Struct } from "effect"
const result = pipe(
{ name: "alice", age: 30, active: true },
Struct.evolve({ name: (s) => s.toUpperCase(), age: (n) => n + 1 })
)
console.log(result) // => { name: "ALICE", age: 31, active: true }

Transforms selected keys with per-key functions; values are preserved.

import { pipe, Struct } from "effect"
console.log(pipe({ name: "Alice", age: 30 }, Struct.evolveKeys({ name: (k) => k.toUpperCase() })))
// => { NAME: "Alice", age: 30 }

Transforms both keys and values together: each function receives (key, value) and returns [newKey, newValue].

import { pipe, Struct } from "effect"
const result = pipe(
{ amount: 100, label: "total" },
Struct.evolveEntries({
amount: (k, v) => [`${k}Cents`, v * 100],
label: (k, v) => [k, v.toUpperCase()]
})
)
console.log(result) // => { amountCents: 10000, label: "TOTAL" }

Declarative key renaming with a static { oldKey: newKey } mapping; unmentioned keys are copied unchanged.

import { pipe, Struct } from "effect"
console.log(
pipe(
{ firstName: "Alice", lastName: "Smith", age: 30 },
Struct.renameKeys({ firstName: "first", lastName: "last" })
)
)
// => { first: "Alice", last: "Smith", age: 30 }

map, mapPick, and mapOmit apply a single transformation to many fields. So the compiler can track how each field’s type changes, the transformation must be a Lambda value created with Struct.lambda — a plain function will not type-check.

The type-level function interface. Extend it with concrete ~lambda.in / ~lambda.out types to describe how values are transformed.

import type { Struct } from "effect"
interface ToString extends Struct.Lambda {
readonly "~lambda.out": string
}

Computes the output type a Lambda produces for a given input type.

import type { Struct } from "effect"
interface ToString extends Struct.Lambda {
readonly "~lambda.out": string
}
type Result = Struct.Apply<ToString, number> // string

Wraps a plain function as a Lambda value for use with map / mapPick / mapOmit. At runtime it is the same function; only the type changes.

import { Struct } from "effect"
interface AsArray extends Struct.Lambda {
<A>(self: A): Array<A>
readonly "~lambda.out": Array<this["~lambda.in"]>
}
const asArray = Struct.lambda<AsArray>((a) => [a])
console.log(asArray(1)) // => [1]

Applies a Lambda to every value in the struct.

import { pipe, Struct } from "effect"
interface AsArray extends Struct.Lambda {
<A>(self: A): Array<A>
readonly "~lambda.out": Array<this["~lambda.in"]>
}
const asArray = Struct.lambda<AsArray>((a) => [a])
console.log(pipe({ width: 10, height: 20 }, Struct.map(asArray)))
// => { width: [10], height: [20] }

Applies a Lambda only to the selected keys; the rest are copied unchanged.

import { pipe, Struct } from "effect"
interface AsArray extends Struct.Lambda {
<A>(self: A): Array<A>
readonly "~lambda.out": Array<this["~lambda.in"]>
}
const asArray = Struct.lambda<AsArray>((a) => [a])
console.log(pipe({ x: 1, y: 2, z: 3 }, Struct.mapPick(["x", "z"], asArray)))
// => { x: [1], y: 2, z: [3] }

Applies a Lambda to all keys except the selected ones.

import { pipe, Struct } from "effect"
interface AsArray extends Struct.Lambda {
<A>(self: A): Array<A>
readonly "~lambda.out": Array<this["~lambda.in"]>
}
const asArray = Struct.lambda<AsArray>((a) => [a])
console.log(pipe({ x: 1, y: 2, z: 3 }, Struct.mapOmit(["y"], asArray)))
// => { x: [1], y: 2, z: [3] }

Builds an Equivalence for a struct from a per-field Equivalence. (Alias of Equivalence.Struct.)

import { Equivalence, Struct } from "effect"
const PersonEq = Struct.makeEquivalence({
name: Equivalence.strictEqual<string>(),
age: Equivalence.strictEqual<number>()
})
console.log(PersonEq({ name: "Alice", age: 30 }, { name: "Alice", age: 30 })) // => true
console.log(PersonEq({ name: "Alice", age: 30 }, { name: "Bob", age: 30 })) // => false

Builds an Order for a struct from a per-field Order. Fields are compared in declaration order; the first non-zero result wins. (Alias of Order.Struct.)

import { Number as Num, String as Str, Struct } from "effect"
const PersonOrder = Struct.makeOrder({ name: Str.Order, age: Num.Order })
console.log(PersonOrder({ name: "Alice", age: 30 }, { name: "Bob", age: 25 })) // => -1

Builds a Combiner for a struct from a per-field Combiner; combining two structs merges each field with its combiner. Pass omitKeyWhen to drop fields whose merged value matches a predicate.

import { Number as Num, String as Str, Struct } from "effect"
const C = Struct.makeCombiner<{ readonly n: number; readonly s: string }>({
n: Num.ReducerSum,
s: Str.ReducerConcat
})
console.log(C.combine({ n: 1, s: "hello" }, { n: 2, s: " world" }))
// => { n: 3, s: "hello world" }

Like makeCombiner, but each field’s initial value comes from its Reducer.initialValue, so you can fold a whole collection of structs into one.

import { Number as Num, String as Str, Struct } from "effect"
const R = Struct.makeReducer<{ readonly n: number; readonly s: string }>({
n: Num.ReducerSum,
s: Str.ReducerConcat
})
console.log(R.combineAll([{ n: 1, s: "a" }, { n: 2, s: "b" }, { n: 3, s: "c" }]))
// => { n: 6, s: "abc" }

A constructor (re-exported under Struct) that builds an object assigning the same value to each of the given keys.

import { Struct } from "effect"
console.log(Struct.Record(["a", "b"], "value")) // => { a: "value", b: "value" }

Tuple works on fixed-length readonly arrays where each position can have a different type. Element access is by numeric index, and the type system tracks the type at every slot.

import { Tuple } from "effect"

Creates a properly typed tuple from its arguments (instead of [...] as const).

import { Tuple } from "effect"
console.log(Tuple.make(10, 20, "red")) // => [10, 20, "red"]

Extracts the element at a given index; the index is constrained to valid positions and the return type tracks the slot’s type.

import { pipe, Tuple } from "effect"
console.log(pipe(Tuple.make(1, true, "hello"), Tuple.get(2))) // => "hello"

Selects elements by index; result order matches the provided indices.

import { Tuple } from "effect"
console.log(Tuple.pick(["a", "b", "c", "d"], [0, 2, 3])) // => ["a", "c", "d"]

Removes elements by index; the rest keep their original order.

import { Tuple } from "effect"
console.log(Tuple.omit(["a", "b", "c", "d"], [1, 3])) // => ["a", "c"]

Appends one element to the end, growing the tuple type to [...T, E].

import { pipe, Tuple } from "effect"
const result = pipe(Tuple.make(1, 2), Tuple.appendElement("end"))
// result type: [number, number, string]
console.log(result) // => [1, 2, "end"]

Concatenates two tuples into [...T1, ...T2], preserving all element types.

import { pipe, Tuple } from "effect"
console.log(pipe(Tuple.make(1, 2), Tuple.appendElements(["a", "b"] as const)))
// => [1, 2, "a", "b"]

Transforms elements by position using an array of functions; positions beyond the array are copied unchanged, and each function may change its element’s type.

import { pipe, Tuple } from "effect"
const result = pipe(
Tuple.make("hello", 42, true),
Tuple.evolve([(s) => s.toUpperCase(), (n) => n * 2])
)
console.log(result) // => ["HELLO", 84, true]

Reorders elements by providing an array of stringified source indices (e.g. ["2", "1", "0"] reverses a 3-tuple), preserving each slot’s type.

import { pipe, Tuple } from "effect"
console.log(pipe(Tuple.make("a", "b", "c"), Tuple.renameIndices(["2", "1", "0"])))
// => ["c", "b", "a"]

Applies a Struct.Lambda to every element. As with Struct.map, the lambda must be created with Struct.lambda.

import { pipe, Struct, Tuple } from "effect"
interface AsArray extends Struct.Lambda {
<A>(self: A): Array<A>
readonly "~lambda.out": Array<this["~lambda.in"]>
}
const asArray = Struct.lambda<AsArray>((a) => [a])
console.log(pipe(Tuple.make(1, "hello", true), Tuple.map(asArray)))
// => [[1], ["hello"], [true]]

Applies a Struct.Lambda only to the selected indices; other elements are copied.

import { pipe, Struct, Tuple } from "effect"
interface AsArray extends Struct.Lambda {
<A>(self: A): Array<A>
readonly "~lambda.out": Array<this["~lambda.in"]>
}
const asArray = Struct.lambda<AsArray>((a) => [a])
console.log(pipe(Tuple.make(1, "hello", true), Tuple.mapPick([0, 2], asArray)))
// => [[1], "hello", [true]]

Applies a Struct.Lambda to all indices except the selected ones.

import { pipe, Struct, Tuple } from "effect"
interface AsArray extends Struct.Lambda {
<A>(self: A): Array<A>
readonly "~lambda.out": Array<this["~lambda.in"]>
}
const asArray = Struct.lambda<AsArray>((a) => [a])
console.log(pipe(Tuple.make(1, "hello", true), Tuple.mapOmit([1], asArray)))
// => [[1], "hello", [true]]

Builds an Equivalence for tuples from per-position Equivalences. (Alias of Equivalence.Tuple.)

import { Equivalence, Tuple } from "effect"
const eq = Tuple.makeEquivalence([
Equivalence.strictEqual<string>(),
Equivalence.strictEqual<number>()
])
console.log(eq(["Alice", 30], ["Alice", 30])) // => true
console.log(eq(["Alice", 30], ["Bob", 30])) // => false

Builds an Order for tuples from per-position Orders, compared left to right. (Alias of Order.Tuple.)

import { Number as Num, String as Str, Tuple } from "effect"
const ord = Tuple.makeOrder([Str.Order, Num.Order])
console.log(ord(["Alice", 30], ["Bob", 25])) // => -1
console.log(ord(["Alice", 30], ["Alice", 30])) // => 0

Builds a Combiner for a tuple from per-position Combiners; combining merges each element with its combiner.

import { Number as Num, String as Str, Tuple } from "effect"
const C = Tuple.makeCombiner<readonly [number, string]>([Num.ReducerSum, Str.ReducerConcat])
console.log(C.combine([1, "hello"], [2, " world"])) // => [3, "hello world"]

Like makeCombiner, but derives the initial value from each position’s Reducer.initialValue, letting you fold a collection of tuples.

import { Number as Num, String as Str, Tuple } from "effect"
const R = Tuple.makeReducer<readonly [number, string]>([Num.ReducerSum, Str.ReducerConcat])
console.log(R.combineAll([[1, "a"], [2, "b"], [3, "c"]])) // => [6, "abc"]

Runtime guard re-exported from Predicate: checks an array has exactly N elements, narrowing it to a fixed-length tuple (length only, not element types).

import { Tuple } from "effect"
const arr: Array<number> = [1, 2, 3]
if (Tuple.isTupleOf(arr, 3)) {
console.log(arr) // arr: [number, number, number]
}

Runtime guard re-exported from Predicate: checks an array has at least N elements, narrowing to a tuple with a minimum length.

import { Tuple } from "effect"
const arr: Array<number> = [1, 2, 3, 4]
if (Tuple.isTupleOfAtLeast(arr, 3)) {
console.log(arr) // arr: [number, number, number, ...number[]]
}