Data access
Read and write data through typed paths with get, set, update, and fn.
Creating paths
Three forms, all producing the same Path<T, V> type:
import { path, unsafePath } from "data-path";
type User = { profile: { name: string }; tags: string[] };
// Annotation form — recommended, infers both T and V
const namePath = path((u: User) => u.profile.name);
// Both-explicit form — use when T and V are needed separately
const tagsPath = path<User, string[]>((u) => u.tags);
// Root path — start here, extend with .to()
const root = path<User>();
const nameViaRoot = root.to((u) => u.profile.name);
// From a raw string — for dynamic contexts (API responses, Zod issues)
const dynamic = unsafePath<User>("profile.name");get
Traverses each segment, returning undefined if any intermediate is null or undefined.
const user: User = { profile: { name: "Alice" }, tags: [] };
namePath.get(user) // "Alice"
namePath.get(null) // undefined — no throw
namePath.get({} as User) // undefined — "profile" is missingThe return type is V | undefined — the path cannot guarantee the value exists at runtime.
set
Returns a structural clone with the value updated at the path. Every object along the path is cloned; everything else is shared.
const updated = namePath.set(user, "Bob");
updated.profile.name // "Bob"
user.profile.name // "Alice" — original unchanged
updated.tags === user.tags // true — unaffected branches are sharedIf a segment points into an array, the array is cloned rather than the parent object:
type AppData = { items: string[] };
const firstItemPath = path((d: AppData) => d.items[0]);
const data: AppData = { items: ["a", "b"] };
const next = firstItemPath.set(data, "x");
next.items // ["x", "b"]
data.items // ["a", "b"] — unchangedupdate
Reads the current value, passes it to the updater, then writes the result.
const incremented = scorePath.update(game, (n) => (n ?? 0) + 1);The updater receives V | undefined — handle the undefined case explicitly when the value might be absent.
On a plain Path, this is equivalent to path.set(data, updater(path.get(data))). On a TemplatePath the equivalence does not hold: .update invokes the updater once per matched item with that item's value, then writes each result back to its own location. See Templates for the per-item form, or use .set(data, constant) if you want to broadcast a single value across every match.
fn
A stable (data: T) => V | undefined function reference. It is assigned once in the constructor and does not change, so it is safe to use as a dependency or pass to array methods:
const users: User[] = [user1, user2, user3];
const names = users.map(namePath.fn); // (string | undefined)[]
// React example — stable reference avoids unnecessary re-renders
const value = useMemo(() => namePath.fn(state), [state]);For template paths (each, deep), .fn returns V[] instead of V | undefined.
String properties
namePath.$ // "profile.name"
namePath.segments // ["profile", "name"]
namePath.length // 2.segments is a readonly array of string | number. Numeric segments represent array indices.
See also
- Templates — bulk reads and writes across collections
- Path Algebra — compose and decompose paths
- Runtime Variables — indices and closures inside lambdas
Last updated on