Rec — Record Utilities
2.54 kB minified + gzipped
Plain JavaScript objects serving as string-keyed dictionaries — typed as Record<string, A> — are
used everywhere in TypeScript applications for configurations, HTTP headers, lookup tables, and JSON
payloads.
However, working with records in standard JavaScript introduces two recurring problems:
- Object operations are imperative or verbose: Transforming values across a record requires
converting the object with
Object.entries(obj), mapping the array of pairs, and rebuilding the object withObject.fromEntries(...). - Dynamic key access is unsafe: Looking up a key with bracket notation (
headers[key]) silently returnsundefinedat runtime when the key is missing, and accessing"__proto__"or"toString"can collide withObject.prototypeproperties unless guarded withObject.hasOwn.
Rec solves both issues by providing pure, curried, data-last utilities over immutable
Readonly<Record<K, A>> objects, returning Maybe for dynamic lookups and protecting against
prototype pollution automatically.
Safe Key Lookup and Inspection
Section titled “Safe Key Lookup and Inspection”Rec.lookup checks own properties via Object.hasOwn and returns the value wrapped in a Maybe
container, making missing keys explicit in the type system:
Because Rec.lookup returns a Maybe, it composes directly with parsing and fallback steps:
To inspect the size or emptiness of a record, use Rec.size, Rec.is.empty, and Rec.is.nonEmpty:
Transforming Values and Keys
Section titled “Transforming Values and Keys”Rec.map transforms every value inside a record while preserving its keys. When the transformation
also needs access to the property key, use Rec.mapWithKey:
When you need to rename keys across a record — such as normalizing HTTP header names or prefixing
configuration keys — Rec.mapKeys transforms each key while preserving its value. To transform both
keys and values in a single pass, Rec.mapEntries maps each (key, value) pair to a new
[newKey, newValue] tuple:
Filtering and Compacting
Section titled “Filtering and Compacting”Rec.filter keeps only the entries whose values satisfy a predicate, while Rec.filterWithKey
passes both the key and the value to the predicate:
When you want to transform values and discard failed or empty results simultaneously,
Rec.filterMap applies a Maybe-returning function to each value and retains only the Some
results. If you already have a record of Maybe<A> values, Rec.compact unwraps all Some entries
and drops all None entries:
Picking, Omitting, and Grouping
Section titled “Picking, Omitting, and Grouping”Rec.pick creates a new record containing only the specified keys (Pick<A, K>), while Rec.omit
creates a new record with the specified keys removed (Omit<A, K>):
To group an array of items into a record keyed by a string selector, use Rec.groupBy:
Merging and Deep Updates
Section titled “Merging and Deep Updates”Rec.merge combines two records into a new object where keys in the second record override keys in
the first. When you want to combine colliding values using a custom function rather than overwriting
them, use Rec.mergeWith:
When updating a deeply nested property inside a record, Rec.updateIn applies a transformation at a
path tuple without verbose nested object spreads:
Keys, Values, Entries, and Conversions
Section titled “Keys, Values, Entries, and Conversions”Rec.keys, Rec.values, and Rec.entries extract typed arrays from a record, while
Rec.from.entries builds a record from [key, value] tuples. To convert a string-keyed record into
an immutable ReadonlyMap<K, A> for use with Dict, use Rec.to.Dict:
Traversing and Sequencing Records
Section titled “Traversing and Sequencing Records”When you validate or transform every value in a record using a function that returns a Maybe or
Result, Rec.traverse collects the results into a single Maybe or Result wrapping the entire
record, short-circuiting on the first failure. Rec.sequence performs the same flip on a record
whose values are already wrapped in Maybe or Result:
Using Rec.traverse.Result and Rec.sequence.Result preserves the error of whichever key fails
first:
Non-Empty Records: Rec.NonEmpty
Section titled “Non-Empty Records: Rec.NonEmpty”When a function requires a compile-time guarantee that a record has at least one key-value entry,
use Rec.NonEmpty<A, K>. You can construct one directly with Rec.NonEmpty.singleton, narrow a
record with the Rec.is.nonEmpty type guard, or refine a record into Maybe<Rec.NonEmpty<A, K>>
via Rec.NonEmpty.from.record:
Extracting keys, values, or entries from a Rec.NonEmpty via Rec.NonEmpty.keys,
Rec.NonEmpty.values, and Rec.NonEmpty.entries returns a guaranteed non-empty array
(Arr.NonEmpty), and Rec.NonEmpty.reduce folds the values without needing an initial seed value:
Because top-level Rec.map and Rec.mapWithKey return general Readonly<Record<K, B>> types, use
Rec.NonEmpty.map and Rec.NonEmpty.mapWithKey when you want transformations to preserve the
Rec.NonEmpty<B, K> brand across a pipeline:
For more details on non-empty data modeling, see the Non-Empty Collections Guide.
Pipeline Composition
Section titled “Pipeline Composition”Here is an end-to-end configuration ingestion pipeline that merges raw environment overrides onto default service limits, normalizes keys to lowercase, parses numeric strings while discarding malformed values, and extracts the active worker limit:
Problems it solves
Section titled “Problems it solves”- Type-safe payload sanitization: When preparing API responses, stripping sensitive fields (such
as password hashes, secret tokens, or internal flags) using
Rec.omitor retaining allowed fields viaRec.picknarrows the TypeScript type statically, preventing accidental access to omitted properties downstream. - Transforming and filtering record values in pipelines: When processing dynamic dictionaries
(such as converting string prices to numbers or stripping empty optional fields),
Rec.map,Rec.filter,Rec.filterMap, andRec.compactapply transformations directly withinpipeworkflows while preserving key structure. - Payload key migration and renaming (
Rec.mapKeys,Rec.mapEntries): When ingesting third-party webhook payloads or normalizing HTTP headers,Rec.mapKeysandRec.mapEntriestransform object keys and values immutably without manualObject.entries/Object.fromEntriesboilerplate. - Safe dynamic property retrieval (
Rec.lookup): When looking up runtime keys in configuration objects, user attributes, or localized string tables, direct index access (obj[key]) returnsundefinedwithout static warnings.Rec.lookupchecksObject.hasOwnand returnsMaybe<V>, requiring explicit presence handling before operating on values. - Merging multi-layer configuration records (
Rec.merge,Rec.mergeWith): Combining default settings with user overrides or accumulating metrics across datasets using custom collision handlers. - Deep immutable updates without spread nesting (
Rec.updateIn): Updating nested properties in deeply structured configuration objects without writing repetitive object spread boilerplate.