data-path
Guides

Templates

Bulk reads and writes across arrays and recursive trees using each and deep.

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().

Wildcards are sentinel symbols, not the strings "*"/"**"

The wildcards inserted by .each() and .deep() are unique Symbol values exported as WILDCARD / DEEP_WILDCARD. They render as "*" / "**" in .toString() and .$ for dot-notation compatibility, but they are NOT the same as the literal strings "*" and "**".

This matters when your data has a real property named "*" (or "**"):

import { path, unsafePath } from "data-path";

// Literal "*" key — no wildcard semantics:
unsafePath<{ "*": number }>("*").get({ "*": 1, "**": 2 });   // 1

// Wildcard sentinel — expands to every key:
path<Record<string, number>>().each().get({ "*": 1, "**": 2 });  // [1, 2]

In short: the only way to get wildcard expansion is .each() / .deep() on a path or template. unsafePath("a.*.b"), { segments: ["*"] }, and (x) => x["*"] are always literal-key lookups.

each — single-level wildcard

.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 — recursive wildcard

.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.

get — bulk read

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 — bulk write

.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 — per-item transform

.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 — resolve to concrete 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.

Chaining wildcards

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 template paths

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

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

See also

Last updated on

On this page