Skip to content

Templates

A template path contains one or more wildcards (* or **). It targets multiple values at once rather than a single leaf. Template paths are created with .each() and .deep().

.each() appends a * wildcard that matches every key of the current collection.

import { path } from "data-path";
type AppData = { users: Array<{ name: string; active: boolean }> };
const allUsersPath = path((d: AppData) => d.users).each();
allUsersPath.$ // "users.*"
const allNamesPath = path((d: AppData) => d.users).each((u) => u.name);
allNamesPath.$ // "users.*.name"

The optional expression argument navigates from each matched item. Without it, .each() targets the items themselves.

.deep() appends a ** wildcard that matches the current value and all descendants recursively.

type Tree = { label: string; children?: Tree[] };
const allLabelsPath = path((t: Tree) => t).deep((n) => n.label);
allLabelsPath.$ // "**.label"

** tries to match the rest of the pattern at the current depth first, then recurses into every child.

On a template path, .get() returns V[] (an array of all matched values):

const data: AppData = {
users: [{ name: "Alice", active: true }, { name: "Bob", active: false }],
};
allNamesPath.get(data) // ["Alice", "Bob"]

.set(data, value) applies the same value to every matched path:

const anonymized = allNamesPath.set(data, "Hidden");
anonymized.users[0].name // "Hidden"
anonymized.users[1].name // "Hidden"
data.users[0].name // "Alice" — original unchanged

.update(data, fn) calls the updater once per matched path, with the current value at that path:

const uppercased = allNamesPath.update(data, (name) => (name ?? "").toUpperCase());
uppercased.users[0].name // "ALICE"
uppercased.users[1].name // "BOB"

Each update is applied to the result of the previous one — later paths see the changes from earlier paths.

.expand(data) walks the data and returns one Path<T, V> per matched leaf:

const paths = allNamesPath.expand(data);
paths[0].$ // "users.0.name"
paths[1].$ // "users.1.name"

expand only visits keys that exist in the data. Missing intermediates are skipped, so .expand() on sparse data returns fewer paths than the structure implies.

Multiple .each() and .deep() calls compose naturally:

type Org = { teams: Array<{ members: Array<{ email: string }> }> };
const allEmailsPath = path((o: Org) => o.teams)
.each((t) => t.members)
.each((m) => m.email);
allEmailsPath.$ // "teams.*.members.*.email"

.fn on a template path returns (data: T) => V[], suitable for .map():

const allNames = teamList.map(allNamesPath.fn); // string[][]