HashMap, HashSet & Trie
HashMap, HashSet, and Trie are persistent immutable collections. Every
update returns a brand-new collection that shares most of its structure with the
old one, so the original is never mutated and copies are cheap.
The defining feature of HashMap and HashSet is that keys/values are compared
with Effect’s Equal and Hash traits, not by
reference. Two structurally-equal objects (for example two Data.struct values
or two branded ids with the same contents) collide as the same key — unlike a
native Map/Set, which key by object identity. Trie is the special case where
keys are always string, which unlocks fast prefix queries.
import { Data, HashMap } from "effect"
// A value-equal key: two structurally identical Data.struct values are equalconst k1 = Data.struct({ org: "acme", repo: "core" })const k2 = Data.struct({ org: "acme", repo: "core" })
const stars = HashMap.empty<typeof k1, number>().pipe( HashMap.set(k1, 42))
console.log(HashMap.get(stars, k2)) // => Option.some(42) -- k2 finds k1's entry
// A native Map keys by reference, so k2 would NOT find k1:const native = new Map([[k1, 42]])console.log(native.get(k2)) // => undefinedBecause lookups are value-based, missing keys are explicit: get returns an
Option, never undefined.
Batched updates
Section titled “Batched updates”Each set/remove/add allocates a new collection. When you apply many updates
in a row, that intermediate allocation is wasted. HashMap (and HashSet
internally) supports a mutation window: beginMutation returns a transient
draft you can mutate in place, and endMutation seals it back into an immutable
value. The mutate helper wraps both for you.
import { HashMap } from "effect"
const base = HashMap.make(["a", 1])
// mutate: one transient draft, sealed automatically — no per-step allocationsconst result = HashMap.mutate(base, (draft) => { HashMap.set(draft, "b", 2) HashMap.set(draft, "c", 3) HashMap.remove(draft, "a")})
console.log(HashMap.size(result)) // => 2console.log(HashMap.size(base)) // => 1 -- original untouched
// The explicit form is equivalent:const draft = HashMap.beginMutation(base)HashMap.set(draft, "b", 2)HashMap.set(draft, "c", 3)const sealed = HashMap.endMutation(draft)console.log(HashMap.size(sealed)) // => 2Prefix queries with Trie
Section titled “Prefix queries with Trie”A Trie<V> is a string-keyed map stored as a prefix tree, so it can answer
“which entries start with this string?” in time proportional to the prefix length
— ideal for autocomplete, command tables, and routing. Iteration is in
alphabetical key order, not insertion order.
import { Trie } from "effect"
const commands = Trie.make( ["commit", "Record changes"], ["checkout", "Switch branches"], ["clone", "Copy a repository"], ["config", "Get and set options"])
// Autocomplete: every command starting with "co" (alphabetical by key)console.log(Trie.toEntriesWithPrefix(commands, "co"))// => [["commit", "Record changes"], ["config", "Get and set options"]]
console.log(Array.from(Trie.keysWithPrefix(commands, "c")))// => ["checkout", "clone", "commit", "config"]HashMap
Section titled “HashMap”A HashMap<Key, Value> is an immutable key-value map backed by a Hash Array
Mapped Trie. Keys are grouped by Hash and matched with Equal. Every operation
below that “changes” the map returns a new instance.
import { HashMap } from "effect"interface HashMap
Section titled “interface HashMap”HashMap<Key, Value> is Iterable<[Key, Value]> and implements Equal,
Pipeable, and Inspectable. The namespace also exposes type-level helpers
HashMap.HashMap.Key, HashMap.HashMap.Value, and HashMap.HashMap.Entry.
const inventory = HashMap.make(["laptop", { qty: 5 }])type Id = HashMap.HashMap.Key<typeof inventory> // => stringtype Item = HashMap.HashMap.Value<typeof inventory> // => { qty: number }isHashMap
Section titled “isHashMap”Type guard: checks whether a value is a HashMap.
console.log(HashMap.isHashMap(HashMap.make(["a", 1]))) // => trueconsole.log(HashMap.isHashMap({ a: 1 })) // => falseisEmpty
Section titled “isEmpty”Returns true when the map has no entries.
console.log(HashMap.isEmpty(HashMap.empty())) // => trueconsole.log(HashMap.isEmpty(HashMap.make(["a", 1]))) // => falseCreates a new empty HashMap.
const map = HashMap.empty<string, number>()console.log(HashMap.size(map)) // => 0Builds a HashMap from a variadic list of [key, value] pairs.
const map = HashMap.make(["a", 1], ["b", 2], ["c", 3])console.log(HashMap.size(map)) // => 3fromIterable
Section titled “fromIterable”Builds a HashMap from any iterable of [key, value] pairs.
const map = HashMap.fromIterable([["a", 1], ["b", 2]])console.log(HashMap.get(map, "a")) // => Option.some(1)Looks up a key, returning Option<Value> (none if absent).
const map = HashMap.make(["a", 1])console.log(HashMap.get(map, "a")) // => Option.some(1)console.log(HashMap.get(map, "z")) // => Option.none()getHash
Section titled “getHash”Like get, but you supply a precomputed hash for the key — useful in hot paths
where the hash is cached.
import { Hash } from "effect"
const map = HashMap.make(["user1", "Alice"])console.log(HashMap.getHash(map, "user1", Hash.string("user1"))) // => Option.some("Alice")getUnsafe
Section titled “getUnsafe”Returns the value directly, throwing if the key is absent. Prefer get
unless presence is guaranteed.
const config = HashMap.make(["url", "https://example.com"])console.log(HashMap.getUnsafe(config, "url")) // => "https://example.com"// HashMap.getUnsafe(config, "missing") // throws "HashMap.getUnsafe: key not found"Checks whether a key has an entry.
const map = HashMap.make(["a", 1])console.log(HashMap.has(map, "a")) // => trueconsole.log(HashMap.has(map, "z")) // => falsehasHash
Section titled “hasHash”Like has, using a precomputed hash. A matching hash still does not override key
equality.
import { Hash } from "effect"
const map = HashMap.make(["Admin", { role: "admin" }])console.log(HashMap.hasHash(map, "Admin", Hash.string("Admin"))) // => trueChecks whether any entry matches a (value, key) predicate.
const map = HashMap.make([1, "a"], [2, "b"])console.log(HashMap.hasBy(map, (value, key) => key === 1 && value === "a")) // => trueconsole.log(HashMap.hasBy(map, (value) => value === "z")) // => falseReturns an IterableIterator of keys (hash order — sort if you need determinism).
const map = HashMap.make(["a", 1], ["b", 2])console.log(Array.from(HashMap.keys(map)).sort()) // => ["a", "b"]values
Section titled “values”Returns an IterableIterator of values.
const map = HashMap.make(["a", 1], ["b", 2])console.log(Array.from(HashMap.values(map)).sort()) // => [1, 2]toValues
Section titled “toValues”Returns the values as an Array (eager Array.from(values(self))).
const map = HashMap.make(["a", 1], ["b", 2])console.log(HashMap.toValues(map).sort()) // => [1, 2]entries
Section titled “entries”Returns an IterableIterator of [key, value] entries.
const map = HashMap.make(["a", 1])console.log(Array.from(HashMap.entries(map))) // => [["a", 1]]toEntries
Section titled “toEntries”Returns the entries as an Array<[key, value]>.
const map = HashMap.make(["a", 1], ["b", 2])console.log(HashMap.toEntries(map).sort()) // => [["a", 1], ["b", 2]]Returns the number of entries.
console.log(HashMap.size(HashMap.make(["a", 1], ["b", 2]))) // => 2Sets a key to a value, returning a new map.
const map = HashMap.make(["a", 1])console.log(HashMap.get(HashMap.set(map, "b", 2), "b")) // => Option.some(2)setMany
Section titled “setMany”Sets many [key, value] pairs at once; later duplicate keys win.
const map = HashMap.make(["a", 1])const next = HashMap.setMany(map, [["b", 2], ["a", 10]])console.log(HashMap.get(next, "a")) // => Option.some(10)remove
Section titled “remove”Removes a key, returning a new map.
const map = HashMap.make(["a", 1], ["b", 2])console.log(HashMap.has(HashMap.remove(map, "a"), "a")) // => falseremoveMany
Section titled “removeMany”Removes all of the given keys.
const map = HashMap.make(["a", 1], ["b", 2], ["c", 3])console.log(HashMap.size(HashMap.removeMany(map, ["a", "c"]))) // => 1modify
Section titled “modify”Updates an existing key’s value with (v) => v; a no-op if the key is absent.
const map = HashMap.make(["a", 1])console.log(HashMap.get(HashMap.modify(map, "a", (v) => v * 3), "a")) // => Option.some(3)modifyAt
Section titled “modifyAt”Inserts, updates, or removes a key using an Option => Option function: receives
some(value) if present (else none); return some to store, none to remove.
import { Option } from "effect"
const map = HashMap.make(["a", 1])const inc = (o: Option.Option<number>) => Option.isSome(o) ? Option.some(o.value + 1) : Option.some(1)console.log(HashMap.get(HashMap.modifyAt(map, "a", inc), "a")) // => Option.some(2)console.log(HashMap.get(HashMap.modifyAt(map, "b", inc), "b")) // => Option.some(1)modifyHash
Section titled “modifyHash”Like modifyAt, but with a precomputed hash.
import { Hash, Option } from "effect"
const map = HashMap.make(["hits", 100])const inc = (o: Option.Option<number>) => Option.isSome(o) ? Option.some(o.value + 1) : Option.some(1)console.log( HashMap.get(HashMap.modifyHash(map, "hits", Hash.string("hits"), inc), "hits")) // => Option.some(101)Combines two maps; on equal keys the value from that (the second map) wins.
const a = HashMap.make(["a", 1], ["b", 2])const b = HashMap.make(["b", 20], ["c", 3])console.log(HashMap.get(HashMap.union(a, b), "b")) // => Option.some(20)beginMutation
Section titled “beginMutation”Returns a transient mutable draft for batched updates. Pair with endMutation.
const draft = HashMap.beginMutation(HashMap.make(["a", 1]))HashMap.set(draft, "b", 2)console.log(HashMap.size(HashMap.endMutation(draft))) // => 2endMutation
Section titled “endMutation”Seals a draft from beginMutation back into an immutable HashMap.
const draft = HashMap.beginMutation(HashMap.empty<string, number>())HashMap.set(draft, "x", 1)const final = HashMap.endMutation(draft)console.log(HashMap.size(final)) // => 1mutate
Section titled “mutate”Runs a callback against a transient draft and returns the sealed result — the
ergonomic form of beginMutation/endMutation.
const result = HashMap.mutate(HashMap.make(["a", 1]), (draft) => { HashMap.set(draft, "b", 2) HashMap.set(draft, "c", 3)})console.log(HashMap.size(result)) // => 3Transforms every value via (value, key) => newValue, returning a new map.
const map = HashMap.make(["a", 1], ["b", 2])const out = HashMap.map(map, (value, key) => `${key}:${value * 2}`)console.log(HashMap.get(out, "a")) // => Option.some("a:2")flatMap
Section titled “flatMap”Maps each entry to a HashMap and merges the results (both maps must share
equality/hash behavior).
const map = HashMap.make(["a", 1])const out = HashMap.flatMap(map, (value, key) => HashMap.make([key + "1", value], [key + "2", value * 2]))console.log(HashMap.get(out, "a2")) // => Option.some(2)forEach
Section titled “forEach”Runs a (value, key) => void side effect for each entry.
const collected: Array<string> = []HashMap.forEach(HashMap.make(["a", 1], ["b", 2]), (v, k) => collected.push(`${k}=${v}`))console.log(collected.sort()) // => ["a=1", "b=2"]reduce
Section titled “reduce”Folds the entries into a single value via (acc, value, key) => acc.
const map = HashMap.make(["a", 1], ["b", 2], ["c", 3])console.log(HashMap.reduce(map, 0, (acc, value) => acc + value)) // => 6filter
Section titled “filter”Keeps only entries matching a (value, key) predicate.
const map = HashMap.make(["a", 1], ["b", 2], ["c", 3])console.log(HashMap.size(HashMap.filter(map, (v) => v % 2 === 1))) // => 2filterMap
Section titled “filterMap”Maps and filters in one pass: return Result.succeed to keep, Result.failVoid
to drop.
import { Result } from "effect"
const map = HashMap.make(["a", 1], ["b", 2], ["c", 3])const evens = HashMap.filterMap(map, (v) => v % 2 === 0 ? Result.succeed(v * 10) : Result.failVoid)console.log(HashMap.get(evens, "b")) // => Option.some(20)compact
Section titled “compact”Drops Option.none values from a HashMap of Options, unwrapping the rest.
import { Option } from "effect"
const map = HashMap.make(["a", Option.some(1)], ["b", Option.none<number>()])console.log(HashMap.size(HashMap.compact(map))) // => 1findFirst
Section titled “findFirst”Returns the first [key, value] matching a predicate as an Option.
const map = HashMap.make(["a", 1], ["b", 2])console.log(HashMap.findFirst(map, (v, k) => k === "b")) // => Option.some(["b", 2])Returns true if any entry matches the predicate.
const map = HashMap.make(["a", 1], ["b", 2])console.log(HashMap.some(map, (v) => v > 1)) // => trueReturns true if all entries match the predicate.
const map = HashMap.make(["a", 1], ["b", 2])console.log(HashMap.every(map, (v) => v > 0)) // => trueHashSet
Section titled “HashSet”A HashSet<A> is an immutable set of unique values, deduplicated by Equal/Hash
(it is backed by a HashMap internally). All combinators return new sets.
import { HashSet } from "effect"interface HashSet
Section titled “interface HashSet”HashSet<Value> is Iterable<Value> and implements Equal, Pipeable, and
Inspectable. The type-level helper HashSet.HashSet.Value extracts the element
type.
const fruits = HashSet.make("apple", "banana")type Fruit = HashSet.HashSet.Value<typeof fruits> // => stringisHashSet
Section titled “isHashSet”Type guard: checks whether a value is a HashSet.
console.log(HashSet.isHashSet(HashSet.make(1, 2))) // => trueconsole.log(HashSet.isHashSet([1, 2])) // => falseCreates an empty HashSet.
console.log(HashSet.size(HashSet.empty<string>())) // => 0Builds a set from a variadic list of values; duplicates collapse.
console.log(HashSet.size(HashSet.make(1, 2, 2, 3))) // => 3fromIterable
Section titled “fromIterable”Builds a set from any iterable; duplicates collapse.
console.log(HashSet.size(HashSet.fromIterable(["a", "b", "a"]))) // => 2Adds a value, returning a new set (no-op if already present).
const set = HashSet.make("a")console.log(HashSet.has(HashSet.add(set, "b"), "b")) // => trueChecks membership using Equal/Hash.
const set = HashSet.make("apple", "banana")console.log(HashSet.has(set, "apple")) // => trueconsole.log(HashSet.has(set, "grape")) // => falseremove
Section titled “remove”Removes a value, returning a new set (no-op if absent).
const set = HashSet.make("a", "b")console.log(HashSet.has(HashSet.remove(set, "a"), "a")) // => falseReturns the number of values.
console.log(HashSet.size(HashSet.make("a", "b", "c"))) // => 3isEmpty
Section titled “isEmpty”Returns true when the set has no values.
console.log(HashSet.isEmpty(HashSet.empty())) // => trueconsole.log(HashSet.isEmpty(HashSet.make("a"))) // => falseReturns all values in either set.
const a = HashSet.make("a", "b")const b = HashSet.make("b", "c")console.log(Array.from(HashSet.union(a, b)).sort()) // => ["a", "b", "c"]intersection
Section titled “intersection”Returns only values present in both sets.
const a = HashSet.make("a", "b", "c")const b = HashSet.make("b", "c", "d")console.log(Array.from(HashSet.intersection(a, b)).sort()) // => ["b", "c"]difference
Section titled “difference”Returns values in the first set that are not in the second.
const a = HashSet.make("a", "b", "c")const b = HashSet.make("b", "d")console.log(Array.from(HashSet.difference(a, b)).sort()) // => ["a", "c"]isSubset
Section titled “isSubset”Returns true if every value of the first set is in the second.
const small = HashSet.make("a", "b")const large = HashSet.make("a", "b", "c")console.log(HashSet.isSubset(small, large)) // => trueconsole.log(HashSet.isSubset(large, small)) // => falseMaps each value; the result may be smaller if values collide.
const set = HashSet.make("apple", "banana", "cherry")console.log(Array.from(HashSet.map(set, (s) => s.length)).sort()) // => [5, 6]filter
Section titled “filter”Keeps only values matching the predicate (supports refinements).
const set = HashSet.make(1, 2, 3, 4)console.log(Array.from(HashSet.filter(set, (n) => n % 2 === 0)).sort()) // => [2, 4]reduce
Section titled “reduce”Folds the values into a single result.
const set = HashSet.make(1, 2, 3, 4)console.log(HashSet.reduce(set, 0, (acc, n) => acc + n)) // => 10Returns true if any value matches the predicate.
console.log(HashSet.some(HashSet.make(1, 2, 3), (n) => n > 2)) // => trueReturns true if all values match the predicate (vacuously true when empty).
console.log(HashSet.every(HashSet.make(2, 4, 6), (n) => n % 2 === 0)) // => trueA Trie<Value> is an immutable string-keyed map stored as a prefix tree.
Iteration and the keys/values/entries getters yield results in alphabetical
key order, regardless of insertion order. Lookups cost is proportional to key
length, and prefix queries are the headline feature.
import { Trie } from "effect"interface Trie
Section titled “interface Trie”Trie<Value> is Iterable<[string, Value]> and implements Equal, Pipeable,
and Inspectable. Updates such as insert and remove return new tries.
const trie = Trie.make(["app", 1], ["apple", 2])for (const [key, value] of trie) console.log(key, value)// => "app 1" then "apple 2"Creates an empty Trie.
console.log(Trie.size(Trie.empty<number>())) // => 0Builds a Trie from a variadic list of [string, value] entries.
const trie = Trie.make(["ca", 0], ["me", 1])console.log(Trie.toEntries(trie)) // => [["ca", 0], ["me", 1]]fromIterable
Section titled “fromIterable”Builds a Trie from any iterable of [string, value] entries.
const trie = Trie.fromIterable([["mind", 2], ["call", 0]])console.log(Trie.toEntries(trie)) // => [["call", 0], ["mind", 2]]insert
Section titled “insert”Inserts (or overwrites) one entry, returning a new Trie.
const trie = Trie.empty<number>().pipe(Trie.insert("call", 0))console.log(Trie.get(trie, "call")) // => Option.some(0)insertMany
Section titled “insertMany”Inserts multiple entries at once.
const trie = Trie.empty<number>().pipe(Trie.insertMany([["she", 0], ["sea", 1]]))console.log(Trie.size(trie)) // => 2Looks up an exact key, returning Option<Value>.
const trie = Trie.make(["call", 0])console.log(Trie.get(trie, "call")) // => Option.some(0)console.log(Trie.get(trie, "ca")) // => Option.none()getUnsafe
Section titled “getUnsafe”Returns the value directly, throwing if the key is absent. Prefer get.
const trie = Trie.make(["call", 0])console.log(Trie.getUnsafe(trie, "call")) // => 0// Trie.getUnsafe(trie, "ca") // throwsChecks whether an exact key exists.
const trie = Trie.make(["call", 0])console.log(Trie.has(trie, "call")) // => trueconsole.log(Trie.has(trie, "ca")) // => falseReturns the number of entries.
console.log(Trie.size(Trie.make(["a", 0], ["b", 1]))) // => 2isEmpty
Section titled “isEmpty”Returns true when the trie has no entries.
console.log(Trie.isEmpty(Trie.empty())) // => trueconsole.log(Trie.isEmpty(Trie.make(["a", 0]))) // => falselongestPrefixOf
Section titled “longestPrefixOf”Returns the longest stored key that is a prefix of the given key, with its value,
as an Option<[string, Value]>.
const trie = Trie.make(["shells", 0], ["sells", 1], ["she", 2])console.log(Trie.longestPrefixOf(trie, "sells")) // => Option.some(["sells", 1])console.log(Trie.longestPrefixOf(trie, "sell")) // => Option.none()Returns an IterableIterator of all keys in alphabetical order.
const trie = Trie.make(["cab", 0], ["abc", 1], ["bca", 2])console.log(Array.from(Trie.keys(trie))) // => ["abc", "bca", "cab"]values
Section titled “values”Returns an IterableIterator of all values, ordered by their keys.
const trie = Trie.make(["call", 0], ["me", 1], ["and", 2])console.log(Array.from(Trie.values(trie))) // => [2, 0, 1] (and, call, me)entries
Section titled “entries”Returns an IterableIterator of [key, value] entries in key order.
const trie = Trie.make(["me", 1], ["call", 0])console.log(Array.from(Trie.entries(trie))) // => [["call", 0], ["me", 1]]toEntries
Section titled “toEntries”Returns all entries as an Array<[string, Value]> (eager).
const trie = Trie.make(["call", 0], ["me", 1])console.log(Trie.toEntries(trie)) // => [["call", 0], ["me", 1]]keysWithPrefix
Section titled “keysWithPrefix”Returns the keys (in order) that start with prefix, including prefix itself if
present.
const trie = Trie.make(["she", 0], ["shells", 1], ["sea", 2], ["shore", 3])console.log(Array.from(Trie.keysWithPrefix(trie, "she"))) // => ["she", "shells"]valuesWithPrefix
Section titled “valuesWithPrefix”Returns the values whose keys start with prefix.
const trie = Trie.make(["she", 0], ["shells", 1], ["sea", 2])console.log(Array.from(Trie.valuesWithPrefix(trie, "she"))) // => [0, 1]entriesWithPrefix
Section titled “entriesWithPrefix”Returns the [key, value] entries whose keys start with prefix.
const trie = Trie.make(["she", 0], ["shells", 1], ["sea", 2])console.log(Array.from(Trie.entriesWithPrefix(trie, "she"))) // => [["she", 0], ["shells", 1]]toEntriesWithPrefix
Section titled “toEntriesWithPrefix”Returns the prefixed [key, value] entries as an Array (eager).
const trie = Trie.make(["shells", 0], ["sells", 1], ["sea", 2], ["she", 3])console.log(Trie.toEntriesWithPrefix(trie, "she")) // => [["she", 3], ["shells", 0]]remove
Section titled “remove”Removes the entry for a key, returning a new Trie (no-op if absent).
const trie = Trie.make(["call", 0], ["me", 1])console.log(Trie.get(Trie.remove(trie, "call"), "call")) // => Option.none()removeMany
Section titled “removeMany”Removes all entries for the given keys.
const trie = Trie.make(["shells", 0], ["sells", 1], ["she", 2])console.log(Trie.toEntries(Trie.removeMany(trie, ["she", "sells"]))) // => [["shells", 0]]modify
Section titled “modify”Updates an existing key’s value via (v) => v; a no-op if the key is absent.
const trie = Trie.make(["she", 2])console.log(Trie.get(Trie.modify(trie, "she", (v) => v + 10), "she")) // => Option.some(12)Transforms every value via (value, key) => newValue, returning a new Trie.
const trie = Trie.make(["shells", 0], ["she", 2])console.log(Trie.toEntries(Trie.map(trie, (v) => v + 1))) // => [["she", 3], ["shells", 1]]filter
Section titled “filter”Keeps only entries matching a (value, key) predicate (supports refinements).
const trie = Trie.make(["shells", 0], ["sells", 1], ["she", 2])console.log(Trie.toEntries(Trie.filter(trie, (v) => v > 1))) // => [["she", 2]]filterMap
Section titled “filterMap”Maps and filters in one pass: Result.succeed keeps, Result.failVoid drops.
import { Result } from "effect"
const trie = Trie.make(["shells", 0], ["sells", 1], ["she", 2])const kept = Trie.filterMap(trie, (v) => v > 1 ? Result.succeed(v) : Result.failVoid)console.log(Trie.toEntries(kept)) // => [["she", 2]]compact
Section titled “compact”Drops Option.none values from a Trie of Options, unwrapping the rest.
import { Option } from "effect"
const trie = Trie.make( ["shells", Option.some(0)], ["sells", Option.none<number>()], ["she", Option.some(2)])console.log(Trie.toEntries(Trie.compact(trie))) // => [["she", 2], ["shells", 0]]reduce
Section titled “reduce”Folds the entries into a single value via (acc, value, key) => acc, in key order.
const trie = Trie.make(["shells", 0], ["sells", 1], ["she", 2])console.log(Trie.reduce(trie, 0, (acc, v) => acc + v)) // => 3console.log(Trie.reduce(trie, "", (acc, _, key) => acc + key)) // => "sellssheshells"forEach
Section titled “forEach”Runs a (value, key) => void side effect for each entry in key order.
let total = 0Trie.forEach(Trie.make(["she", 2], ["sells", 1]), (n, key) => { total += n + key.length})console.log(total) // => 11 (she: 2+3, sells: 1+5)