Path algebra
Path algebra is useful when building reusable components or functions that work with paths at different levels of a data structure. A component can hold a relative path; its parent supplies the base; merge combines them.
Each example below is self-contained — copy any fence and it should compile on its own.
to — extend a path
Section titled “to — extend a path”Appends a relative path expressed as a lambda or an existing Path:
import { path } from "data-path";
type User = { profile: { name: string; age: number } };
const profilePath = path((u: User) => u.profile);const namePath = profilePath.to((p) => p.name);
namePath.$; // "profile.name"You can also pass a pre-built Path<V, U>:
import { path } from "data-path";
type User = { profile: { name: string } };type Profile = User["profile"];
const profilePath = path((u: User) => u.profile);const nameRelative = path((p: Profile) => p.name);
const fullName = profilePath.to(nameRelative); // Path<User, string>fullName.$; // "profile.name"On a template path, .to() returns a template path so expansion is preserved:
import { path } from "data-path";
type AppData = { users: Array<{ name: string }> };const data: AppData = { users: [{ name: "Alice" }, { name: "Bob" }] };
const allNames = path((u: AppData) => u.users).each().to((u) => u.name);allNames.$; // "users.*.name"allNames.get(data); // ["Alice", "Bob"]merge — overlap-aware concatenation
Section titled “merge — overlap-aware concatenation”.merge(other) appends other to the path, deduplicating any suffix/prefix overlap. This is the right tool when two paths were built independently but may share a common junction:
import { path } from "data-path";
type Employee = { profile: { name: string } };type Company = { departments: Array<{ employees: Employee[] }>;};
const employeePath = path((c: Company) => c.departments[0].employees[5]);const nameSub = path((e: Employee) => e.profile.name);
const full = employeePath.merge(nameSub);full.$; // "departments.0.employees.5.profile.name"If the tail of the first path matches the head of the second, the overlap is removed. The algorithm finds the longest suffix of this that equals a prefix of other and joins at that point:
import { path } from "data-path";
type Tree = { a: { b: { c: { d: string } } }; b: { c: { d: string } } };
const a = path((x: Tree) => x.a.b.c);const b = path((x: Tree) => x.b.c.d);
a.merge(b).$; // "a.b.c.d" — longest overlap "b.c" collapsed, not just "c"If there is no overlap, .merge() is a plain concatenation — equivalent to .to().
subtract — remove a known prefix
Section titled “subtract — remove a known prefix”.subtract(prefix) returns a new path with the prefix stripped, or null if the prefix does not match:
import { path } from "data-path";
type Department = { name: string };type Company = { departments: Department[] };
const full = path((c: Company) => c.departments[0].name);const prefix = path((c: Company) => c.departments[0]);
const relative = full.subtract(prefix); // Path<Department, string> | nullrelative?.$; // "name"
full.subtract(path((c: Company) => c.departments[1])); // null — mismatchThe return type changes the root from Company to Department — the resolved type of the prefix. This is the key DX benefit: the returned path is self-contained relative to the stripped prefix and can be passed to any function expecting a Path<Department, V>.
slice — segment range
Section titled “slice — segment range”.slice(start?, end?) follows Array.prototype.slice semantics:
import { path } from "data-path";
type Company = { departments: Array<{ employees: Array<{ profile: { name: string } }>; }>;};
const p = path((c: Company) => c.departments[0].employees[5].profile.name);p.$; // "departments.0.employees.5.profile.name"
p.slice(0, 3).$; // "departments.0.employees"p.slice(-2).$; // "profile.name"Useful when a path was built dynamically and you need a subsection of it.
parent — drop the last segment
Section titled “parent — drop the last segment”import { path } from "data-path";
type User = { profile: { name: string } };
const namePath = path((u: User) => u.profile.name);
namePath.parent()?.$; // "profile"namePath.parent()?.parent()?.$; // "" (root path)namePath.parent()?.parent()?.parent(); // null — already at root.parent() returns null on a root (zero-segment) path.
See also
Section titled “See also”- Relational —
startsWith,covers,matchfor comparing paths - Templates —
toon template paths