data-path 1.0.2 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,444 +1,97 @@
1
1
  # data-path
2
2
 
3
- A simple, modern, zero-dependency, and fully type-safe library for building, comparing, and manipulating object property paths using TypeScript lambda expressions.
3
+ Type-safe object property paths in TypeScript build, compare, and manipulate with lambda expressions. Zero dependencies.
4
4
 
5
- [![npm version](https://badge.fury.io/js/data-path.svg)](https://badge.fury.io/js/data-path)
6
- [![License: ISC](https://img.shields.io/badge/License-ISC-blue.svg)](https://opensource.org/licenses/ISC)
5
+ [![npm version](https://img.shields.io/npm/v/data-path.svg)](https://www.npmjs.com/package/data-path)
6
+ [![CI](https://github.com/sergeyshmakov/data-path/actions/workflows/pr.yml/badge.svg)](https://github.com/sergeyshmakov/data-path/actions/workflows/pr.yml)
7
+ [![Bundle size](https://img.shields.io/bundlephobia/minzip/data-path)](https://bundlephobia.com/package/data-path)
8
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.0+-3178c6.svg?logo=typescript&logoColor=white)](https://www.typescriptlang.org/)
9
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
10
 
8
- ## ⚠️ The Problem
11
+ **Documentation:** https://sergeyshmakov.github.io/data-path/
9
12
 
10
- When working with deep object structures, forms, or nested state, we often rely on string-based paths (e.g., `"users.0.name"` or `"company.departments[1].budget"`). This approach is fundamentally flawed in modern TypeScript development:
13
+ ---
11
14
 
12
- - **No Type Safety:** String paths are opaque to the compiler. A typo goes unnoticed until runtime.
13
- - **Refactoring Nightmares:** Renaming a property doesn't update string literals scattered across your codebase.
14
- - **No Autocomplete:** Your IDE cannot guide you through the object structure.
15
- - **No Mathematics:** It is difficult to programmatically determine if path `A` is a child of path `B`, or if they overlap.
15
+ ## Before / after
16
16
 
17
- ## 🚀 The Solution
18
-
19
- `data-path` solves this by using **Proxy-based lambda expressions** to capture paths. It gives you 100% type safety, IDE autocomplete, and a rich API for interacting with data and other paths.
20
-
21
- ```typescript
22
- import { path } from "data-path";
23
-
24
- type User = {
25
- id: string;
26
- profile: {
27
- firstName: string;
28
- lastName: string;
29
- };
30
- tags: string[];
31
- };
32
-
33
- // 1. Create a path with full IDE autocomplete
34
- const firstNamePath = path<User>(p => p.profile.firstName);
35
-
36
- // 2. Output as a string (e.g., for form libraries)
37
- console.log(firstNamePath.$); // "profile.firstName"
38
-
39
- // 3. Read data safely (no "Cannot read properties of undefined" errors)
40
- const user = {
41
- id: "1",
42
- profile: { firstName: "Alice", lastName: "Smith" },
43
- tags: [],
44
- };
45
- console.log(firstNamePath.get(user)); // "Alice"
46
-
47
- // 4. Update data immutably (returns a new structural clone)
48
- const updatedUser = firstNamePath.set(user, "Bob");
49
- console.log(updatedUser.profile.firstName); // "Bob"
17
+ ```ts
18
+ // Before — string literals, invisible to the compiler
19
+ register("users.0.profile.firstName");
20
+ table.getColumn("contact.email");
21
+ set(state => ({ ...state, settings: { ...state.settings, theme } }));
50
22
  ```
51
23
 
52
- ## 📦 Installation
53
-
54
- ```bash
55
- npm install data-path
24
+ ```ts
25
+ // After — typed, IDE-autocompleted, refactor-safe
26
+ register(path((u: FormData) => u.users[0].profile.firstName).$);
27
+ table.getColumn(emailPath.$);
28
+ set(state => themePath.set(state, theme));
56
29
  ```
57
30
 
58
- _(or use `yarn add data-path`, `pnpm add data-path`, `bun add data-path`)_
31
+ ## What it is
59
32
 
60
- ## 🤖 AI Ready
33
+ A zero-dependency TypeScript library that captures object property paths via proxy-based lambdas. Build a path once — use it as a string, read and write data through it, compose paths together, or match one against another.
61
34
 
62
- This package is available in [Context7](https://context7.com/) MCP, so AI assistants can load it directly into context when working with your object property paths.
35
+ - Typed root to leaf renaming a property breaks the path at compile time
36
+ - Safe `get`, immutable `set`, read-modify-write `update`
37
+ - Template paths (`each`, `deep`) for bulk operations across collections and trees
38
+ - Path algebra: `merge`, `subtract`, `slice`, `to`
39
+ - Runtime indices and closure variables work natively inside lambdas
63
40
 
64
- It also ships an [Agent Skills](https://agentskills.io/) – compatible skill. Install it so your AI assistant loads data-path guidance:
41
+ ## Install
65
42
 
66
43
  ```bash
67
- npx ctx7 skills install /sergeyshmakov/data-path data-path
68
- ```
69
-
70
- The skill lives in [skills/data-path/SKILL.md](skills/data-path/SKILL.md).
71
-
72
- ## 💡 Philosophy
73
-
74
- - **Stack Agnostic:** Pure data manipulation. Works perfectly with React, Vue, Node.js, or vanilla JavaScript.
75
- - **Zero Dependencies:** A tiny, efficient footprint that doesn't bloat your bundle.
76
- - **Fully Type-Safe:** Built strictly for TypeScript. If the structure changes, the compiler will instantly catch broken paths.
77
- - **Immutable:** All `.set()` operations return structurally cloned objects, making it the perfect companion for modern state managers (Redux, Zustand) and reactive frameworks.
78
-
79
- ## 📚 Core API
80
-
81
- ### 🏷️ API Cheatsheet
82
-
83
- | API | Description |
84
- |-----|-------------|
85
- | **Creation** | |
86
- | `path<T>()` | Create root path |
87
- | `path<T>(p => p.a.b)` | Create path from lambda, generic type |
88
- | `path((p: T) => p.a.b)` | Create path from lambda, infer type |
89
- | `path(base, p => p.c)` | Extend existing path |
90
- | `unsafePath<T>("a.b")` | Create path from raw string |
91
- | **Properties** | |
92
- | `path.$` | String representation (e.g. `"users.0.name"`) |
93
- | `path.segments` | Array of segments |
94
- | `path.length` | Number of segments |
95
- | `path.fn` | Accessor function for `.map()`, `.filter()` |
96
- | **Data Access** | |
97
- | `path.get(data)` | Read value at path (returns `undefined` if missing) |
98
- | `path.set(data, value)` | Immutable write, returns new object |
99
- | **Traversal** | |
100
- | `path.to(p => p.x)` | Extend path from current value |
101
- | `path.each(p => p.x)` | Template: match all items in collection |
102
- | `path.each().to(p => p.x)` | Same as above |
103
- | `path.deep(node => node.id)` | Template: match property at any depth |
104
- | **Manipulation** | |
105
- | `path.merge(other)` | Append path (deduplicates overlap) |
106
- | `path.subtract(other)` | Remove prefix/suffix, or `null` |
107
- | `path.slice(start?, end?)` | Slice segments (like `Array.prototype.slice`) |
108
- | **Relational** | |
109
- | `path.startsWith(other)` | True if path is prefix |
110
- | `path.includes(other)` | True if path contains other |
111
- | `path.equals(other)` | True if paths are identical |
112
- | `path.match(other)` | Returns `{ relation, params }` or `null` |
113
- | **Template-only** | |
114
- | `templatePath.expand(data)` | Resolve template to concrete paths |
115
-
116
- ### Path Creation
117
-
118
- You can create paths starting from the root of a type, using a lambda expression, or unsafely from a raw string.
119
-
120
- ```typescript
121
- // From root
122
- const root = path<User>();
123
- const profile = root.to(p => p.profile);
124
-
125
- // Direct lambda
126
- const tagsPath = path<User>(p => p.tags[0]);
127
-
128
- // Infer type from argument (often preferred)
129
- const lastNamePath = path((user: User) => user.profile.lastName);
130
-
131
- // From raw string (dynamic contexts)
132
- import { unsafePath } from "data-path";
133
- const dynamicPath = unsafePath<User>("profile.firstName");
134
- ```
135
-
136
- ### Data Access
137
-
138
- ```typescript
139
- const namePath = path<User>(p => p.profile.firstName);
140
-
141
- // Read
142
- namePath.get(user); // "Alice"
143
-
144
- // Accessor shorthand (perfect for arrays)
145
- const users = [user1, user2];
146
- const names = users.map(namePath.fn);
147
-
148
- // Write (Immutable)
149
- const updated = namePath.set(user, "Alice 2.0");
150
- ```
151
-
152
- ### Manipulation
153
-
154
- You can programmatically compose, subtract, and slice paths. This is ideal when working with reusable components that don't know their absolute location in a global store.
155
-
156
- ```typescript
157
- // 1. We have a specific record's path
158
- const employeePath = path<Company>(p => p.departments[0].employees[5]);
159
-
160
- // 2. We have a generic sub-path
161
- const nameSubPath = path<Employee>(p => p.profile.firstName);
162
-
163
- // Merge them! (Smart deduplication if they overlap)
164
- const absoluteNamePath = employeePath.merge(nameSubPath);
165
- console.log(absoluteNamePath.$); // "departments.0.employees.5.profile.firstName"
166
-
167
- // Subtract a base path to find the relative path
168
- const relative = absoluteNamePath.subtract(employeePath);
169
- console.log(relative?.$); // "profile.firstName"
170
-
171
- // Slice segments (just like Array.prototype.slice)
172
- const sliced = absoluteNamePath.slice(0, 3);
173
- console.log(sliced.$); // "departments.0.employees"
174
- ```
175
-
176
- ### Templates & Wildcards
177
-
178
- Templates allow you to target multiple elements at once (e.g., all items in an array, or all properties matching a name deep in a tree). This unlocks powerful bulk-operations.
179
-
180
- ```typescript
181
- type AppData = { users: Array<{ id: string; name: string }> };
182
- const data: AppData = {
183
- users: [
184
- { id: "1", name: "Alice" },
185
- { id: "2", name: "Bob" },
186
- ],
187
- };
188
-
189
- // 1. Create a template targeting ALL users
190
- const allUsersPath = path<AppData>(p => p.users).each();
191
- console.log(allUsersPath.$); // "users.*"
192
-
193
- // 2. Create a template targeting ALL user names
194
- const allNamesPath = path<AppData>(p => p.users).each(u => u.name);
195
- console.log(allNamesPath.$); // "users.*.name"
196
-
197
- // 3. Bulk Read: extract an array of all matched values
198
- const names = allNamesPath.get(data);
199
- console.log(names); // ["Alice", "Bob"]
200
-
201
- // 4. Bulk Write: immutably update all matches in one go!
202
- const anonymizedData = allNamesPath.set(data, "Hidden");
203
- console.log(anonymizedData.users[0].name); // "Hidden"
204
- console.log(anonymizedData.users[1].name); // "Hidden"
205
-
206
- // 5. Expand: resolve the template into concrete paths based on actual data
207
- const concretePaths = allNamesPath.expand(data);
208
- console.log(concretePaths.map(p => p.$));
209
- // ["users.0.name", "users.1.name"]
210
-
211
- // 6. Deep Scan: target a property at any depth
212
- const deepIds = path<AppData>().deep(node => node.id);
213
- console.log(deepIds.$); // "**.id"
214
- ```
215
-
216
- ### Runtime Variables & Indices
217
-
218
- Because `data-path` executes the lambda once during creation to build the path, you can seamlessly use runtime variables, indexers, and local scope variables directly inside the path definition.
219
-
220
- ```typescript
221
- function getUserPropertyPath(userIdx: number, property: "name" | "email") {
222
- // Capture function arguments directly in the path!
223
- return path<AppData>(p => p.users[userIdx][property]);
224
- }
225
-
226
- const namePath = getUserPropertyPath(2, "name");
227
- console.log(namePath.$); // "users.2.name"
228
-
229
- const emailPath = getUserPropertyPath(5, "email");
230
- console.log(emailPath.$); // "users.5.email"
231
- ```
232
-
233
- ### Relational Algebra
234
-
235
- Path algebra is extremely useful for permissions, validation, and complex UI logic where you need to know how two paths relate to each other.
236
-
237
- ```typescript
238
- const userPath = path<User>();
239
- const profilePath = userPath.to(p => p.profile);
240
- const namePath = userPath.to(p => p.profile.firstName);
241
-
242
- // Check relationships
243
- namePath.startsWith(profilePath); // true
244
- profilePath.includes(namePath); // true
245
- namePath.includes(profilePath); // false
246
- namePath.equals(namePath); // true
247
-
248
- // .match() provides detailed relational context:
249
- // returns 'includes', 'included-by', 'equals', 'parent', 'child', or null
250
- namePath.match(profilePath); // { relation: 'child', params: {} }
251
- profilePath.match(namePath); // { relation: 'parent', params: {} }
252
- ```
253
-
254
- ### Utility Types
255
-
256
- The library exports several TypeScript types that are useful when writing helper functions or React components that accept paths as props:
257
-
258
- ```typescript
259
- import type { Path, TemplatePath, ResolvedType } from "data-path";
260
-
261
- // 1. Accept a specific path structure
262
- function NameInput({ fieldPath }: { fieldPath: Path<User, string> }) {
263
- return <input name={fieldPath.$} />;
264
- }
265
-
266
- // 2. Extract the resolved value type from an existing path
267
- const agePath = path<User>(p => p.profile.age);
268
- type Age = ResolvedType<typeof agePath>; // number
269
- ```
270
-
271
- ## 💼 Real-World Examples
272
-
273
- ### 1. React Hook Form
274
-
275
- Bind deeply nested fields with 100% type safety. Use runtime indices (like `i` from `useFieldArray`'s map) directly inside the path lambda. React Hook Form's `register` accepts dot-notation names (e.g. `users.0.firstName`).
276
-
277
- ```tsx
278
- import { useForm, useFieldArray } from "react-hook-form";
279
- import { path } from "data-path";
280
-
281
- type FormValues = { users: Array<{ id: string; firstName: string }> };
282
-
283
- function UsersForm() {
284
- const { register, control } = useForm<FormValues>({
285
- defaultValues: { users: [{ id: "1", firstName: "" }] },
286
- });
287
- const { fields } = useFieldArray({ control, name: "users" });
288
-
289
- return (
290
- <form>
291
- {fields.map((field, i) => {
292
- const namePath = path<FormValues>(p => p.users[i].firstName);
293
- return (
294
- <input
295
- key={field.id}
296
- {...register(namePath.$)}
297
- placeholder="First name"
298
- />
299
- );
300
- })}
301
- </form>
302
- );
303
- }
304
- ```
305
-
306
- ### 2. TanStack Form
307
-
308
- Define exact field accessors for complex interactive forms, even inside deeply nested arrays. TanStack Form's `Field` uses `name` (dot-notation for nested paths) and a render prop with `field.state.value` and `field.handleChange`.
309
-
310
- ```tsx
311
- import { useForm } from "@tanstack/react-form";
312
- import { path } from "data-path";
313
-
314
- type FormValues = { users: Array<{ firstName: string }> };
315
-
316
- function UsersForm() {
317
- const form = useForm<FormValues>({
318
- defaultValues: { users: [{ firstName: "" }] },
319
- onSubmit: ({ value }) => console.log(value),
320
- });
321
-
322
- return (
323
- <form>
324
- {form.state.values.users.map((_, i) => {
325
- const namePath = path<FormValues>(p => p.users[i].firstName);
326
- return (
327
- <form.Field key={i} name={namePath.$}>
328
- {field => (
329
- <input
330
- name={field.name}
331
- value={field.state.value}
332
- onChange={e =>
333
- field.handleChange(e.target.value)
334
- }
335
- />
336
- )}
337
- </form.Field>
338
- );
339
- })}
340
- </form>
341
- );
342
- }
343
- ```
344
-
345
- ### 3. Zustand
346
-
347
- Update deeply nested state easily without needing Immer. Pass a function to `set` that receives the current state and returns the updated state via `path.set()`.
348
-
349
- ```typescript
350
- import { create } from "zustand";
351
- import { path } from "data-path";
352
-
353
- type StoreState = {
354
- settings: { profile: { theme: string } };
355
- setTheme: (theme: string) => void;
356
- };
357
-
358
- const themePath = path<StoreState>(p => p.settings.profile.theme);
359
-
360
- const useStore = create<StoreState>(set => ({
361
- settings: { profile: { theme: "light" } },
362
- setTheme: newTheme => set(state => themePath.set(state, newTheme)),
363
- }));
44
+ npm install data-path
364
45
  ```
365
46
 
366
- ### 4. React `useState`
47
+ Requirements: Node `>=20`, TypeScript `>=5.0`
367
48
 
368
- Update deeply nested state with the functional updater. React requires a new object reference; `path.set()` returns a structural clone so you avoid manual spreading.
49
+ ## Quick start
369
50
 
370
- ```tsx
371
- import { useState } from "react";
51
+ ```ts
372
52
  import { path } from "data-path";
373
53
 
374
- type AppState = { settings: { profile: { theme: string } } };
375
-
376
- const themePath = path<AppState>(p => p.settings.profile.theme);
54
+ type User = { profile: { firstName: string; lastName: string }; tags: string[] };
377
55
 
378
- function ThemeToggle() {
379
- const [state, setState] = useState<AppState>({
380
- settings: { profile: { theme: "light" } },
381
- });
56
+ const firstNamePath = path((u: User) => u.profile.firstName);
382
57
 
383
- const setTheme = (theme: string) => {
384
- setState(prev => themePath.set(prev, theme));
385
- };
386
-
387
- return <button onClick={() => setTheme("dark")}>{state.settings.profile.theme}</button>;
388
- }
58
+ firstNamePath.$ // "profile.firstName"
59
+ firstNamePath.get(user) // "Alice" | undefined
60
+ firstNamePath.set(user, "Bob") // returns a new User — original unchanged
61
+ firstNamePath.fn // stable (u: User) => string | undefined
389
62
  ```
390
63
 
391
- ### 5. Zod / Validation Mapping
392
-
393
- Map Zod validation errors to specific UI fields. Zod's `ZodError` has an `issues` array; each issue has a `path` (e.g. `["user", "age"]`) that you can join to compare with `data-path`.
394
-
395
- ```typescript
396
- import { z } from "zod";
397
- import { unsafePath, path } from "data-path";
64
+ ## Works well with
398
65
 
399
- const schema = z.object({
400
- user: z.object({ age: z.number().min(18) }),
401
- });
402
- type FormData = z.infer<typeof schema>;
403
- const agePath = path<FormData>(p => p.user.age);
66
+ | Package | How it helps |
67
+ |---------|--------------|
68
+ | [React Hook Form](https://sergeyshmakov.github.io/data-path/integrations/react-hook-form/) | Type-safe field names for `register`, `watch`, `setValue` |
69
+ | [TanStack Form](https://sergeyshmakov.github.io/data-path/integrations/tanstack-form/) | Typed field names with runtime index support |
70
+ | [TanStack Table](https://sergeyshmakov.github.io/data-path/integrations/tanstack-table/) | Typed column accessors — no manual `id` strings |
71
+ | [Zustand](https://sergeyshmakov.github.io/data-path/integrations/zustand/) | Immutable nested state updates without Immer |
72
+ | [Zod](https://sergeyshmakov.github.io/data-path/integrations/zod/) | Map `ZodError.issues` paths to specific form fields |
73
+ | [React `useState`](https://sergeyshmakov.github.io/data-path/integrations/react-usestate/) | Structural clones for deeply nested state |
404
74
 
405
- const result = schema.safeParse(data);
406
- if (!result.success) {
407
- for (const issue of result.error.issues) {
408
- const errorPath = unsafePath<FormData>(issue.path.join("."));
409
- if (errorPath.equals(agePath)) {
410
- console.log("Age must be at least 18!");
411
- }
412
- }
413
- }
414
- ```
415
-
416
- ### 6. TanStack Table
417
-
418
- Define type-safe column accessors. `createColumnHelper.accessor()` accepts an accessor function; use `path.fn` for the extractor and `path.$` for the column `id` (required when using an accessor function).
75
+ ## Guides
419
76
 
420
- ```typescript
421
- import { createColumnHelper } from "@tanstack/react-table";
422
- import { path } from "data-path";
77
+ - [Data access](https://sergeyshmakov.github.io/data-path/guides/data-access/) — `get`, `set`, `update`, `fn`
78
+ - [Templates](https://sergeyshmakov.github.io/data-path/guides/templates/) `each`, `deep`, bulk writes across collections
79
+ - [Path algebra](https://sergeyshmakov.github.io/data-path/guides/path-algebra/) `merge`, `subtract`, `slice`, `to`
80
+ - [Relational](https://sergeyshmakov.github.io/data-path/guides/relational/) — `startsWith`, `covers`, `match`
81
+ - [Runtime variables](https://sergeyshmakov.github.io/data-path/guides/runtime-variables/) — dynamic indices and closures
423
82
 
424
- type User = { id: string; contact: { email: string } };
83
+ ## AI tooling
425
84
 
426
- const columnHelper = createColumnHelper<User>();
427
- const emailPath = path<User>(p => p.contact.email);
85
+ This package is available in [Context7](https://context7.com/) and documented in a [Cubic wiki](https://www.cubic.dev/wikis/sergeyshmakov/data-path). An [Agent Skills](https://agentskills.io/)-compatible skill is included:
428
86
 
429
- const columns = [
430
- columnHelper.accessor(emailPath.fn, {
431
- id: emailPath.$,
432
- header: "Email",
433
- cell: info => info.getValue(),
434
- }),
435
- ];
87
+ ```bash
88
+ npx ctx7 skills install /sergeyshmakov/data-path data-path
436
89
  ```
437
90
 
438
- ## 🤝 Contributing
91
+ ## Contributing
439
92
 
440
- We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for more details.
93
+ See [CONTRIBUTING.md](CONTRIBUTING.md).
441
94
 
442
- ## 📄 License
95
+ ## License
443
96
 
444
- This project is licensed under the [ISC License](LICENSE).
97
+ [MIT License](LICENSE)