Skip to content

Relational

Relational methods are useful for permissions checks, validation mapping, and UI logic where you need to know how two paths relate — whether one is a prefix of another, whether they overlap, or what structural relationship they have.

Think of every path as a location in the data tree. A shorter path like profile covers a wider subtree; a deeper path like profile.name points at a specific spot inside that subtree. startsWith and covers are direction-reversed views of the same containment:

  • deeperPath.startsWith(widerPath) — does the deeper location fall under the wider one?
  • widerPath.covers(deeperPath) — does the wider location contain the deeper one?

(covers is not like Array.prototype.includes; it asks about data-tree containment, not element membership.)

Returns true if the path begins with every segment of other.

import { path } from "data-path";
type User = { profile: { name: string; age: number } };
const namePath = path((u: User) => u.profile.name);
const profilePath = path((u: User) => u.profile);
namePath.startsWith(profilePath) // true — "profile.name" starts with "profile"
profilePath.startsWith(namePath) // false

The argument can be a Path, a lambda, or a { segments } object.

Returns true if this path’s location covers other — i.e. this path is a prefix of other (the inverse direction of startsWith).

profilePath.covers(namePath) // true — "profile" covers "profile.name"
namePath.covers(profilePath) // false

When one path contains wildcards (from .each() or .deep()), use whichever side reads most naturally:

Called onArgumentReturns true when
template.covers(concrete)template’s pattern covers the concrete path
concrete.startsWith(template)concrete falls under the template prefix
interface R { items: Array<{ name: string }> }
const allNames = path((r: R) => r.items).each((i: { name: string }) => i.name);
const one = path((r: R) => r.items[0].name);
allNames.covers(one) // true — the template covers this concrete path
one.startsWith(allNames) // true — the concrete falls under the template prefix

The two are logically equivalent — pick the form that reads naturally for your use case.

For ** (deep wildcard), any number of intermediate segments can match:

interface Root { tree: { a: { b: { c: string } } } }
const deep = path((r: Root) => r.tree).deep(); // ["tree", "**"]
const leaf = path((r: Root) => r.tree.a.b.c); // ["tree", "a", "b", "c"]
leaf.startsWith(deep) // true — "**" matches "a.b.c"
deep.covers(leaf) // true

Returns true if both paths have identical segments.

const p1 = path((u: User) => u.profile.name);
const p2 = path((u: User) => u.profile.name);
p1.equals(p2) // true
p1.equals(profilePath) // false

Returns a MatchResult describing the structural relationship, or null if there is none.

type MatchResult = {
relation: "equals" | "parent" | "child" | "covers" | "covered-by";
};
namePath.match(profilePath) // { relation: "child" } — name is under profile
profilePath.match(namePath) // { relation: "parent" } — profile contains name
namePath.match(namePath) // { relation: "equals" }
const unrelated = path((u: User) => u.profile.age);
namePath.match(unrelated) // null — no relationship

Relation meanings:

RelationMeaning
"equals"Both paths are identical
"parent"this is a prefix of other
"child"other is a prefix of this
"covers"this contains a wildcard that matches other
"covered-by"other contains a wildcard that matches this
type Store = { users: User[]; settings: { theme: string } };
const settingsPath = path((s: Store) => s.settings);
function canWrite(targetPath: Path<Store, unknown>): boolean {
// Allow writes only outside the settings subtree
return !targetPath.startsWith(settingsPath);
}
canWrite(path((s: Store) => s.users[0].name)) // true
canWrite(path((s: Store) => s.settings.theme)) // false