Dict — Dictionary Utilities
2.42 kB minified + gzipped
JavaScript provides two primary ways to associate keys with values: plain objects ({}) and the
built-in Map.
A Map is essential when you need non-string keys (such as numeric IDs, composite objects, or
symbols), guaranteed insertion-order iteration, or frequent additions and removals without prototype
pollution risks. However, using native Map instances in TypeScript pipelines introduces three
problems:
- Mutation in place: Calling
.set()or.delete()mutates the underlyingMapinstance, creating shared-state bugs across components and breaking change detection in UI frameworks. - Data-first prototype methods: Native
Maphas no.map()or.filter()methods, forcing you to spread entries into arrays (new Map([...map].filter(...))) for every transformation. - Unsafe
.get()lookups: Callingmap.get(key)returnsV | undefined, shifting the burden of null-checking onto every caller and conflating missing keys with keys whose value isundefined.
Dict solves these limitations by operating over standard ReadonlyMap<K, V> instances with pure,
curried, data-last functions. Every modification returns a fresh ReadonlyMap without mutating
the original, and every key lookup returns an explicit Maybe<V>.
Creating Dictionaries
Section titled “Creating Dictionaries”You can construct a ReadonlyMap<K, V> using Dict.empty, Dict.singleton, or the converters in
Dict.from:
You can also build a dictionary directly from an array of items using Dict.groupBy, which groups
elements by a key selector function into a ReadonlyMap<K, Arr.NonEmpty<A>>:
Safe Lookups and Inspection
Section titled “Safe Lookups and Inspection”Instead of returning undefined when a key is absent, Dict.lookup returns Some(value) when the
key exists in the dictionary and None when it does not:
When you only need to check whether a key exists without wrapping its value in a Maybe, use
Dict.has. To inspect entry counts, emptiness, or extract arrays of keys, values, and entries, use
Dict.size, Dict.is.empty, Dict.is.nonEmpty, Dict.keys, Dict.values, and Dict.entries:
Transforming Values and Keys
Section titled “Transforming Values and Keys”Dict.map applies a function to every value in the dictionary, returning a new ReadonlyMap with
the same keys. When the transformation also depends on the key, use Dict.mapWithKey:
To transform only the keys of a dictionary, use Dict.mapKeys. To transform both keys and values
simultaneously in a single pass, use Dict.mapEntries:
Filtering and Compacting
Section titled “Filtering and Compacting”Dict.filter retains entries whose values satisfy a predicate, and Dict.filterWithKey passes both
the key and value to the predicate:
When mapping values with a parser or validator that returns Maybe<B>, Dict.filterMap transforms
and filters the dictionary in a single pass, keeping only unwrapped Some values. If you already
hold a ReadonlyMap<K, Maybe<A>>, Dict.compact unwraps all Some entries and discards all None
entries:
Modifying Entries Immutably
Section titled “Modifying Entries Immutably”Every modification function in Dict leaves the input dictionary untouched and returns a new
ReadonlyMap. Use Dict.insert to add or overwrite a key, Dict.remove to delete a key, and
Dict.upsert when updating a key depends on whether an existing value is already present:
Combining Set Algebra and Merging
Section titled “Combining Set Algebra and Merging”When reconciling two dictionaries, Dict provides three set-theoretic combinators plus a custom
collision merger:
Dict.union(other)(dict) combines all entries from both dictionaries, with values in other
overwriting matching keys in dict. Dict.intersection(other)(dict) keeps only keys that exist in
both dictionaries (preserving values from dict). Dict.difference(other)(dict) keeps only keys in
dict that do not exist in other:
When colliding keys should be combined with a calculation rather than overwritten, Dict.mergeWith
accepts a combiner callback (first, second) => combined:
Reducing and Exporting
Section titled “Reducing and Exporting”To fold a dictionary into a single summary value, Dict.reduce iterates over values, while
Dict.reduceWithKey passes (accumulator, value, key) to the reducer. To convert a string-keyed
dictionary back into a plain JavaScript Readonly<Record<K, V>>, use Dict.to.record:
Non-Empty Dictionaries: Dict.NonEmpty
Section titled “Non-Empty Dictionaries: Dict.NonEmpty”When a function requires a dictionary guaranteed to contain at least one entry, use the branded type
Dict.NonEmpty<K, V>. You can create one directly with Dict.NonEmpty.singleton, narrow a
ReadonlyMap with the Dict.is.nonEmpty type guard, or refine an existing map into
Maybe<Dict.NonEmpty<K, V>> via Dict.NonEmpty.from.map:
Operating on Dict.NonEmpty<K, V> unlocks guaranteed non-empty array extractors
(Dict.NonEmpty.keys, Dict.NonEmpty.values, Dict.NonEmpty.entries), seedless reduction
(Dict.NonEmpty.reduce), and non-emptiness-preserving transformations (Dict.NonEmpty.map,
Dict.NonEmpty.mapWithKey):
For more details on non-empty data modeling across collections, see the Non-Empty Collections Guide.
Pipeline Composition
Section titled “Pipeline Composition”Here is an end-to-end metrics aggregation pipeline that merges regional request counters, filters out idle endpoints, normalizes route keys, and exports a plain JSON-serializable summary record:
Problems it solves
Section titled “Problems it solves”- Entity caching with non-string keys: Plain JavaScript objects coerce all keys into strings,
which corrupts numeric database IDs or composite identifiers.
Dictpreserves typed non-string keys while providing guaranteed insertion-order iteration. - Safe map lookup in pipeline chains (
Dict.lookup): Calling.get(key)on a standardMapreturnsV | undefined, which easily escapes into downstream arithmetic or method calls.Dict.lookupwraps lookups inMaybe<V>, enforcing compile-time absence handling before values can be used. - Immutable updates without shared-state mutation (
Dict.insert,Dict.remove,Dict.upsert): Mutating a nativeMapwith.set()or.delete()modifies shared references in place.Dictreturns freshReadonlyMapinstances on every update and provides atomic initialize-or-update semantics viaDict.upsert. - Pipeline transformation of key-value stores (
Dict.map,Dict.mapKeys,Dict.mapEntries,Dict.filterMap,Dict.compact,Dict.mergeWith): Filtering invalid entries, merging configuration layers with custom collision resolvers, or transforming map keys and values in data pipelines without converting to arrays and back. - Dictionary set algebra (
Dict.union,Dict.intersection,Dict.difference): Reconciling local entity caches with remote server state by finding updated, overlapping, or removed map entries without mutating input maps.