Ramonda

Walking a path

A chain records hops. Nothing is read, copied or wrapped while you build it — the whole path is known before a single object is touched, which is what lets one walk copy each level at most once.

.get(key) Descends into a property.
.at(index) Descends into one element. Negative counts from the end.
.where(pred) Descends into every element the predicate accepts.

The array hops exist only when the focused value is an array, so .where() on an object is a compile error at the call site rather than a runtime miss.

where matches all of them

This is the part that differs most from writing the update by hand:

// Every draft post gets the title, in ONE walk. `posts` is copied exactly once
// however many matched, and the posts that did not match keep their identity.
focusOn(state)
  .get("posts")
  .where((post) => post.draft)
  .get("title")
  .set("TODO");

remove follows the same rule — where(…).remove() drops every match in one pass.

Because it matches all of them, where cannot stop early. When you already know the position, at(i) skips the scan, and on a long array that is the difference between a full pass and none.

Narrowing is explicit

focusOn(state)
  .get("values")
  .where<string>((value) => typeof value === "string")
  .update((value) => value.toUpperCase()); // value is string

There is deliberately no overload that infers a type guard from the predicate. Since TypeScript 5.5 a plain arrow gets a type predicate inferred for it, which made where((tag) => tag === "js") on a string[] focus the literal type "js" — and the natural next line, .set("ts"), failed to compile. The narrowing nobody asked for broke the write it was meant to serve.

Optional values on the way

A path steps through a nullable value rather than being stopped by one, so an optional property in the middle does not make everything below it uncallable:

interface State { profile: { name: string } | null }

focusOn(state).get("profile").get("name").set("Ada");

If profile really is null at runtime, nothing changes and development reports which hop could not be reached.

Reading

focusOn(state).get("posts").where((p) => p.id === 102).get("title").value();
// "Second post" — or undefined if the path resolves to nothing

focusOn(state).get("posts").where(Boolean).get("title").values();
// ["First post", "Second post"]

value() is the first focused value, values() is all of them. Reads are silent: asking for a path that does not exist is a fair question with a fair answer, so nothing is reported. Writes are the ones that report a miss, because there the miss means an edit did not happen.

Next

  • Updating — the operations, and forking a path with and.