Skip to content

Map and Record

import { pipe } from '@stopcock/fp'
import * as MapOps from '@stopcock/fp/map'
import * as Record from '@stopcock/fp/record'

Map and Record expose 42 dual operations between them. The complete dual-call catalogue lists both shapes of each operation.

Three lookups, three different answers to “what if it is not there?”:

MapOps.get(scores, 'ada') // Option<number>
pipe(scores, MapOps.get('ada')) // Option<number>
MapOps.getOrUndefined(scores, 'ada') // number | undefined
pipe(scores, MapOps.getOrUndefined('ada')) // number | undefined
MapOps.getOrElse(scores, 'ada', () => 0) // number
pipe(
scores,
MapOps.getOrElse('ada', () => 0),
) // number

getOrElse is lazy. The fallback runs at most once, and only when the key is genuinely absent, so an expensive default costs nothing on a hit:

const directConfig = MapOps.getOrElse(cache, url, () => parseExpensiveDefault())
const pipedConfig = pipe(
cache,
MapOps.getOrElse(url, () => parseExpensiveDefault()),
)

A key whose stored value is undefined is present. All three operations agree on that: get returns Some(undefined), getOrUndefined returns undefined, and getOrElse returns undefined without calling the fallback. If you need to tell “missing” from “stored undefined”, use get.

Every lookup accepts the map first for a direct call, or curries the key first so it drops straight into pipe:

pipe(
scores,
MapOps.getOrElse('ada', () => 0),
)

Both hold string- and symbol-keyed data, so the split is about what the value means, not about the runtime shape.

Reach for Record when the keys are data and every value has the same type: a lookup table, a counter, an index built at runtime. It is the cheaper contract: it works in enumerable own properties on null-prototype objects and does not carry descriptor, accessor, or prototype machinery.

Reach for Object when the keys are the shape: a struct with a known set of differently-typed fields, especially when you need nested path writes, symbol preservation, or descriptor-faithful clones.

There is no Record path helper. Nested writes into homogeneous data are Obj.setPath’s job, and a Record-flavoured copy of it would be the same traversal with a narrower type.