Skip to content

Rec — record utilities

Plain JavaScript objects used as maps — Record<string, A> — are one of the most common data structures in any TypeScript codebase. Rec is a small collection of utilities for working with them in pipelines: data-last, curried, and returning Option wherever a key might not exist.

Rec.lookup retrieves a value by key and returns Option to make the absence explicit:

import { Rec, Option } from "@nlozgachev/pipekit/Core";
import { pipe } from "@nlozgachev/pipekit/Composition";

const settings = { theme: "dark", lang: "en" };

pipe(settings, Rec.lookup("theme")); // Some("dark")
pipe(settings, Rec.lookup("font"));  // None — not undefined

This composes naturally with Option operations:

pipe(
  config,
  Rec.lookup("timeout"),           // Option<string>
  Option.chain(parseNumber),       // Option<number>
  Option.getOrElse(30_000),
);

map transforms every value in a record, preserving keys:

pipe({ a: 1, b: 2, c: 3 }, Rec.map((n) => n * 10));
// { a: 10, b: 20, c: 30 }

mapWithKey receives both key and value:

pipe({ a: 1, b: 2 }, Rec.mapWithKey((key, val) => `${key}=${val}`));
// { a: "a=1", b: "b=2" }

filter keeps entries where the predicate passes:

pipe({ a: 1, b: 2, c: 3 }, Rec.filter((n) => n > 1));
// { b: 2, c: 3 }

filterWithKey receives both key and value:

pipe(
  { a: 1, b: 0, c: 3 },
  Rec.filterWithKey((key, val) => key !== "a" && val > 0),
); // { c: 3 }

pick returns a new record with only the specified keys:

pipe({ a: 1, b: 2, c: 3 }, Rec.pick("a", "c")); // { a: 1, c: 3 }

omit returns a new record with the specified keys removed:

pipe({ a: 1, b: 2, c: 3 }, Rec.omit("b")); // { a: 1, c: 3 }

Both are type-safe: pick returns Pick<A, K> and omit returns Omit<A, K>, so the resulting type reflects exactly which keys are present.

merge combines two records. Keys in the second record take precedence over the first:

pipe(
  { a: 1, b: 2 },
  Rec.merge({ b: 99, c: 3 }),
); // { a: 1, b: 99, c: 3 }
const rec = { x: 10, y: 20 };

Rec.keys(rec);    // ["x", "y"]
Rec.values(rec);  // [10, 20]
Rec.entries(rec); // [["x", 10], ["y", 20]]

fromEntries is the inverse — builds a record from key-value pairs:

Rec.fromEntries([["a", 1], ["b", 2]]); // { a: 1, b: 2 }

entries and fromEntries pair well when you want to transform both keys and values by converting to entries, mapping, and converting back:

pipe(
  { firstName: "Alice", lastName: "Smith" },
  Rec.entries,
  (entries) => entries.map(([k, v]) => [k.toUpperCase(), v] as const),
  Rec.fromEntries,
); // { FIRSTNAME: "Alice", LASTNAME: "Smith" }
Rec.isEmpty({ a: 1 }); // false
Rec.isEmpty({});        // true
Rec.size({ a: 1, b: 2 }); // 2

Because all Rec functions are curried and data-last, they chain naturally:

const result = pipe(
  rawConfig,
  Rec.filter((v) => v !== null),
  Rec.mapWithKey((key, val) => `${key}: ${val}`),
  Rec.omit("debug", "internal"),
);

Each step produces a new record — no mutation, no intermediate variables.