data-path 1.0.3 → 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/LICENSE +17 -3
- package/README.md +60 -409
- package/dist/index.cjs +429 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +376 -0
- package/dist/index.d.ts +243 -186
- package/dist/index.js +221 -349
- package/dist/index.js.map +1 -0
- package/package.json +53 -12
- package/dist/index.d.mts +0 -319
- package/dist/index.mjs +0 -499
package/LICENSE
CHANGED
|
@@ -1,8 +1,22 @@
|
|
|
1
|
-
|
|
1
|
+
MIT License
|
|
2
2
|
|
|
3
3
|
Copyright (c) 2026 Sergei Shmakov
|
|
4
4
|
https://github.com/sergeyshmakov
|
|
5
5
|
|
|
6
|
-
Permission
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
7
12
|
|
|
8
|
-
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,446 +1,97 @@
|
|
|
1
1
|
# data-path
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Type-safe object property paths in TypeScript — build, compare, and manipulate with lambda expressions. Zero dependencies.
|
|
4
4
|
|
|
5
|
-
[](https://www.npmjs.com/package/data-path)
|
|
6
|
+
[](https://github.com/sergeyshmakov/data-path/actions/workflows/pr.yml)
|
|
7
|
+
[](https://bundlephobia.com/package/data-path)
|
|
8
|
+
[](https://www.typescriptlang.org/)
|
|
9
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
10
|
|
|
8
|
-
|
|
11
|
+
**Documentation:** https://sergeyshmakov.github.io/data-path/
|
|
9
12
|
|
|
10
|
-
|
|
13
|
+
---
|
|
11
14
|
|
|
12
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
## 🤖 AI Ready
|
|
31
|
+
## What it is
|
|
61
32
|
|
|
62
|
-
|
|
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.
|
|
63
34
|
|
|
64
|
-
|
|
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
|
|
65
40
|
|
|
66
|
-
|
|
41
|
+
## Install
|
|
67
42
|
|
|
68
43
|
```bash
|
|
69
|
-
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
The skill lives in [skills/data-path/SKILL.md](skills/data-path/SKILL.md).
|
|
73
|
-
|
|
74
|
-
## 💡 Philosophy
|
|
75
|
-
|
|
76
|
-
- **Stack Agnostic:** Pure data manipulation. Works perfectly with React, Vue, Node.js, or vanilla JavaScript.
|
|
77
|
-
- **Zero Dependencies:** A tiny, efficient footprint that doesn't bloat your bundle.
|
|
78
|
-
- **Fully Type-Safe:** Built strictly for TypeScript. If the structure changes, the compiler will instantly catch broken paths.
|
|
79
|
-
- **Immutable:** All `.set()` operations return structurally cloned objects, making it the perfect companion for modern state managers (Redux, Zustand) and reactive frameworks.
|
|
80
|
-
|
|
81
|
-
## 📚 Core API
|
|
82
|
-
|
|
83
|
-
### 🏷️ API Cheatsheet
|
|
84
|
-
|
|
85
|
-
| API | Description |
|
|
86
|
-
|-----|-------------|
|
|
87
|
-
| **Creation** | |
|
|
88
|
-
| `path<T>()` | Create root path |
|
|
89
|
-
| `path<T>(p => p.a.b)` | Create path from lambda, generic type |
|
|
90
|
-
| `path((p: T) => p.a.b)` | Create path from lambda, infer type |
|
|
91
|
-
| `path(base, p => p.c)` | Extend existing path |
|
|
92
|
-
| `unsafePath<T>("a.b")` | Create path from raw string |
|
|
93
|
-
| **Properties** | |
|
|
94
|
-
| `path.$` | String representation (e.g. `"users.0.name"`) |
|
|
95
|
-
| `path.segments` | Array of segments |
|
|
96
|
-
| `path.length` | Number of segments |
|
|
97
|
-
| `path.fn` | Accessor function for `.map()`, `.filter()` |
|
|
98
|
-
| **Data Access** | |
|
|
99
|
-
| `path.get(data)` | Read value at path (returns `undefined` if missing) |
|
|
100
|
-
| `path.set(data, value)` | Immutable write, returns new object |
|
|
101
|
-
| **Traversal** | |
|
|
102
|
-
| `path.to(p => p.x)` | Extend path from current value |
|
|
103
|
-
| `path.each(p => p.x)` | Template: match all items in collection |
|
|
104
|
-
| `path.each().to(p => p.x)` | Same as above |
|
|
105
|
-
| `path.deep(node => node.id)` | Template: match property at any depth |
|
|
106
|
-
| **Manipulation** | |
|
|
107
|
-
| `path.merge(other)` | Append path (deduplicates overlap) |
|
|
108
|
-
| `path.subtract(other)` | Remove prefix/suffix, or `null` |
|
|
109
|
-
| `path.slice(start?, end?)` | Slice segments (like `Array.prototype.slice`) |
|
|
110
|
-
| **Relational** | |
|
|
111
|
-
| `path.startsWith(other)` | True if path is prefix |
|
|
112
|
-
| `path.includes(other)` | True if path contains other |
|
|
113
|
-
| `path.equals(other)` | True if paths are identical |
|
|
114
|
-
| `path.match(other)` | Returns `{ relation, params }` or `null` |
|
|
115
|
-
| **Template-only** | |
|
|
116
|
-
| `templatePath.expand(data)` | Resolve template to concrete paths |
|
|
117
|
-
|
|
118
|
-
### Path Creation
|
|
119
|
-
|
|
120
|
-
You can create paths starting from the root of a type, using a lambda expression, or unsafely from a raw string.
|
|
121
|
-
|
|
122
|
-
```typescript
|
|
123
|
-
// From root
|
|
124
|
-
const root = path<User>();
|
|
125
|
-
const profile = root.to(p => p.profile);
|
|
126
|
-
|
|
127
|
-
// Direct lambda
|
|
128
|
-
const tagsPath = path<User>(p => p.tags[0]);
|
|
129
|
-
|
|
130
|
-
// Infer type from argument (often preferred)
|
|
131
|
-
const lastNamePath = path((user: User) => user.profile.lastName);
|
|
132
|
-
|
|
133
|
-
// From raw string (dynamic contexts)
|
|
134
|
-
import { unsafePath } from "data-path";
|
|
135
|
-
const dynamicPath = unsafePath<User>("profile.firstName");
|
|
136
|
-
```
|
|
137
|
-
|
|
138
|
-
### Data Access
|
|
139
|
-
|
|
140
|
-
```typescript
|
|
141
|
-
const namePath = path<User>(p => p.profile.firstName);
|
|
142
|
-
|
|
143
|
-
// Read
|
|
144
|
-
namePath.get(user); // "Alice"
|
|
145
|
-
|
|
146
|
-
// Accessor shorthand (perfect for arrays)
|
|
147
|
-
const users = [user1, user2];
|
|
148
|
-
const names = users.map(namePath.fn);
|
|
149
|
-
|
|
150
|
-
// Write (Immutable)
|
|
151
|
-
const updated = namePath.set(user, "Alice 2.0");
|
|
152
|
-
```
|
|
153
|
-
|
|
154
|
-
### Manipulation
|
|
155
|
-
|
|
156
|
-
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.
|
|
157
|
-
|
|
158
|
-
```typescript
|
|
159
|
-
// 1. We have a specific record's path
|
|
160
|
-
const employeePath = path<Company>(p => p.departments[0].employees[5]);
|
|
161
|
-
|
|
162
|
-
// 2. We have a generic sub-path
|
|
163
|
-
const nameSubPath = path<Employee>(p => p.profile.firstName);
|
|
164
|
-
|
|
165
|
-
// Merge them! (Smart deduplication if they overlap)
|
|
166
|
-
const absoluteNamePath = employeePath.merge(nameSubPath);
|
|
167
|
-
console.log(absoluteNamePath.$); // "departments.0.employees.5.profile.firstName"
|
|
168
|
-
|
|
169
|
-
// Subtract a base path to find the relative path
|
|
170
|
-
const relative = absoluteNamePath.subtract(employeePath);
|
|
171
|
-
console.log(relative?.$); // "profile.firstName"
|
|
172
|
-
|
|
173
|
-
// Slice segments (just like Array.prototype.slice)
|
|
174
|
-
const sliced = absoluteNamePath.slice(0, 3);
|
|
175
|
-
console.log(sliced.$); // "departments.0.employees"
|
|
176
|
-
```
|
|
177
|
-
|
|
178
|
-
### Templates & Wildcards
|
|
179
|
-
|
|
180
|
-
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.
|
|
181
|
-
|
|
182
|
-
```typescript
|
|
183
|
-
type AppData = { users: Array<{ id: string; name: string }> };
|
|
184
|
-
const data: AppData = {
|
|
185
|
-
users: [
|
|
186
|
-
{ id: "1", name: "Alice" },
|
|
187
|
-
{ id: "2", name: "Bob" },
|
|
188
|
-
],
|
|
189
|
-
};
|
|
190
|
-
|
|
191
|
-
// 1. Create a template targeting ALL users
|
|
192
|
-
const allUsersPath = path<AppData>(p => p.users).each();
|
|
193
|
-
console.log(allUsersPath.$); // "users.*"
|
|
194
|
-
|
|
195
|
-
// 2. Create a template targeting ALL user names
|
|
196
|
-
const allNamesPath = path<AppData>(p => p.users).each(u => u.name);
|
|
197
|
-
console.log(allNamesPath.$); // "users.*.name"
|
|
198
|
-
|
|
199
|
-
// 3. Bulk Read: extract an array of all matched values
|
|
200
|
-
const names = allNamesPath.get(data);
|
|
201
|
-
console.log(names); // ["Alice", "Bob"]
|
|
202
|
-
|
|
203
|
-
// 4. Bulk Write: immutably update all matches in one go!
|
|
204
|
-
const anonymizedData = allNamesPath.set(data, "Hidden");
|
|
205
|
-
console.log(anonymizedData.users[0].name); // "Hidden"
|
|
206
|
-
console.log(anonymizedData.users[1].name); // "Hidden"
|
|
207
|
-
|
|
208
|
-
// 5. Expand: resolve the template into concrete paths based on actual data
|
|
209
|
-
const concretePaths = allNamesPath.expand(data);
|
|
210
|
-
console.log(concretePaths.map(p => p.$));
|
|
211
|
-
// ["users.0.name", "users.1.name"]
|
|
212
|
-
|
|
213
|
-
// 6. Deep Scan: target a property at any depth
|
|
214
|
-
const deepIds = path<AppData>().deep(node => node.id);
|
|
215
|
-
console.log(deepIds.$); // "**.id"
|
|
216
|
-
```
|
|
217
|
-
|
|
218
|
-
### Runtime Variables & Indices
|
|
219
|
-
|
|
220
|
-
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.
|
|
221
|
-
|
|
222
|
-
```typescript
|
|
223
|
-
function getUserPropertyPath(userIdx: number, property: "name" | "email") {
|
|
224
|
-
// Capture function arguments directly in the path!
|
|
225
|
-
return path<AppData>(p => p.users[userIdx][property]);
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
const namePath = getUserPropertyPath(2, "name");
|
|
229
|
-
console.log(namePath.$); // "users.2.name"
|
|
230
|
-
|
|
231
|
-
const emailPath = getUserPropertyPath(5, "email");
|
|
232
|
-
console.log(emailPath.$); // "users.5.email"
|
|
233
|
-
```
|
|
234
|
-
|
|
235
|
-
### Relational Algebra
|
|
236
|
-
|
|
237
|
-
Path algebra is extremely useful for permissions, validation, and complex UI logic where you need to know how two paths relate to each other.
|
|
238
|
-
|
|
239
|
-
```typescript
|
|
240
|
-
const userPath = path<User>();
|
|
241
|
-
const profilePath = userPath.to(p => p.profile);
|
|
242
|
-
const namePath = userPath.to(p => p.profile.firstName);
|
|
243
|
-
|
|
244
|
-
// Check relationships
|
|
245
|
-
namePath.startsWith(profilePath); // true
|
|
246
|
-
profilePath.includes(namePath); // true
|
|
247
|
-
namePath.includes(profilePath); // false
|
|
248
|
-
namePath.equals(namePath); // true
|
|
249
|
-
|
|
250
|
-
// .match() provides detailed relational context:
|
|
251
|
-
// returns 'includes', 'included-by', 'equals', 'parent', 'child', or null
|
|
252
|
-
namePath.match(profilePath); // { relation: 'child', params: {} }
|
|
253
|
-
profilePath.match(namePath); // { relation: 'parent', params: {} }
|
|
254
|
-
```
|
|
255
|
-
|
|
256
|
-
### Utility Types
|
|
257
|
-
|
|
258
|
-
The library exports several TypeScript types that are useful when writing helper functions or React components that accept paths as props:
|
|
259
|
-
|
|
260
|
-
```typescript
|
|
261
|
-
import type { Path, TemplatePath, ResolvedType } from "data-path";
|
|
262
|
-
|
|
263
|
-
// 1. Accept a specific path structure
|
|
264
|
-
function NameInput({ fieldPath }: { fieldPath: Path<User, string> }) {
|
|
265
|
-
return <input name={fieldPath.$} />;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
// 2. Extract the resolved value type from an existing path
|
|
269
|
-
const agePath = path<User>(p => p.profile.age);
|
|
270
|
-
type Age = ResolvedType<typeof agePath>; // number
|
|
271
|
-
```
|
|
272
|
-
|
|
273
|
-
## 💼 Real-World Examples
|
|
274
|
-
|
|
275
|
-
### 1. React Hook Form
|
|
276
|
-
|
|
277
|
-
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`).
|
|
278
|
-
|
|
279
|
-
```tsx
|
|
280
|
-
import { useForm, useFieldArray } from "react-hook-form";
|
|
281
|
-
import { path } from "data-path";
|
|
282
|
-
|
|
283
|
-
type FormValues = { users: Array<{ id: string; firstName: string }> };
|
|
284
|
-
|
|
285
|
-
function UsersForm() {
|
|
286
|
-
const { register, control } = useForm<FormValues>({
|
|
287
|
-
defaultValues: { users: [{ id: "1", firstName: "" }] },
|
|
288
|
-
});
|
|
289
|
-
const { fields } = useFieldArray({ control, name: "users" });
|
|
290
|
-
|
|
291
|
-
return (
|
|
292
|
-
<form>
|
|
293
|
-
{fields.map((field, i) => {
|
|
294
|
-
const namePath = path<FormValues>(p => p.users[i].firstName);
|
|
295
|
-
return (
|
|
296
|
-
<input
|
|
297
|
-
key={field.id}
|
|
298
|
-
{...register(namePath.$)}
|
|
299
|
-
placeholder="First name"
|
|
300
|
-
/>
|
|
301
|
-
);
|
|
302
|
-
})}
|
|
303
|
-
</form>
|
|
304
|
-
);
|
|
305
|
-
}
|
|
306
|
-
```
|
|
307
|
-
|
|
308
|
-
### 2. TanStack Form
|
|
309
|
-
|
|
310
|
-
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`.
|
|
311
|
-
|
|
312
|
-
```tsx
|
|
313
|
-
import { useForm } from "@tanstack/react-form";
|
|
314
|
-
import { path } from "data-path";
|
|
315
|
-
|
|
316
|
-
type FormValues = { users: Array<{ firstName: string }> };
|
|
317
|
-
|
|
318
|
-
function UsersForm() {
|
|
319
|
-
const form = useForm<FormValues>({
|
|
320
|
-
defaultValues: { users: [{ firstName: "" }] },
|
|
321
|
-
onSubmit: ({ value }) => console.log(value),
|
|
322
|
-
});
|
|
323
|
-
|
|
324
|
-
return (
|
|
325
|
-
<form>
|
|
326
|
-
{form.state.values.users.map((_, i) => {
|
|
327
|
-
const namePath = path<FormValues>(p => p.users[i].firstName);
|
|
328
|
-
return (
|
|
329
|
-
<form.Field key={i} name={namePath.$}>
|
|
330
|
-
{field => (
|
|
331
|
-
<input
|
|
332
|
-
name={field.name}
|
|
333
|
-
value={field.state.value}
|
|
334
|
-
onChange={e =>
|
|
335
|
-
field.handleChange(e.target.value)
|
|
336
|
-
}
|
|
337
|
-
/>
|
|
338
|
-
)}
|
|
339
|
-
</form.Field>
|
|
340
|
-
);
|
|
341
|
-
})}
|
|
342
|
-
</form>
|
|
343
|
-
);
|
|
344
|
-
}
|
|
345
|
-
```
|
|
346
|
-
|
|
347
|
-
### 3. Zustand
|
|
348
|
-
|
|
349
|
-
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()`.
|
|
350
|
-
|
|
351
|
-
```typescript
|
|
352
|
-
import { create } from "zustand";
|
|
353
|
-
import { path } from "data-path";
|
|
354
|
-
|
|
355
|
-
type StoreState = {
|
|
356
|
-
settings: { profile: { theme: string } };
|
|
357
|
-
setTheme: (theme: string) => void;
|
|
358
|
-
};
|
|
359
|
-
|
|
360
|
-
const themePath = path<StoreState>(p => p.settings.profile.theme);
|
|
361
|
-
|
|
362
|
-
const useStore = create<StoreState>(set => ({
|
|
363
|
-
settings: { profile: { theme: "light" } },
|
|
364
|
-
setTheme: newTheme => set(state => themePath.set(state, newTheme)),
|
|
365
|
-
}));
|
|
44
|
+
npm install data-path
|
|
366
45
|
```
|
|
367
46
|
|
|
368
|
-
|
|
47
|
+
Requirements: Node `>=20`, TypeScript `>=5.0`
|
|
369
48
|
|
|
370
|
-
|
|
49
|
+
## Quick start
|
|
371
50
|
|
|
372
|
-
```
|
|
373
|
-
import { useState } from "react";
|
|
51
|
+
```ts
|
|
374
52
|
import { path } from "data-path";
|
|
375
53
|
|
|
376
|
-
type
|
|
377
|
-
|
|
378
|
-
const themePath = path<AppState>(p => p.settings.profile.theme);
|
|
54
|
+
type User = { profile: { firstName: string; lastName: string }; tags: string[] };
|
|
379
55
|
|
|
380
|
-
|
|
381
|
-
const [state, setState] = useState<AppState>({
|
|
382
|
-
settings: { profile: { theme: "light" } },
|
|
383
|
-
});
|
|
56
|
+
const firstNamePath = path((u: User) => u.profile.firstName);
|
|
384
57
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
return <button onClick={() => setTheme("dark")}>{state.settings.profile.theme}</button>;
|
|
390
|
-
}
|
|
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
|
|
391
62
|
```
|
|
392
63
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
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`.
|
|
64
|
+
## Works well with
|
|
396
65
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
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 |
|
|
400
74
|
|
|
401
|
-
|
|
402
|
-
user: z.object({ age: z.number().min(18) }),
|
|
403
|
-
});
|
|
404
|
-
type FormData = z.infer<typeof schema>;
|
|
405
|
-
const agePath = path<FormData>(p => p.user.age);
|
|
75
|
+
## Guides
|
|
406
76
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
console.log("Age must be at least 18!");
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
```
|
|
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
|
|
417
82
|
|
|
418
|
-
|
|
83
|
+
## AI tooling
|
|
419
84
|
|
|
420
|
-
|
|
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:
|
|
421
86
|
|
|
422
|
-
```
|
|
423
|
-
|
|
424
|
-
import { path } from "data-path";
|
|
425
|
-
|
|
426
|
-
type User = { id: string; contact: { email: string } };
|
|
427
|
-
|
|
428
|
-
const columnHelper = createColumnHelper<User>();
|
|
429
|
-
const emailPath = path<User>(p => p.contact.email);
|
|
430
|
-
|
|
431
|
-
const columns = [
|
|
432
|
-
columnHelper.accessor(emailPath.fn, {
|
|
433
|
-
id: emailPath.$,
|
|
434
|
-
header: "Email",
|
|
435
|
-
cell: info => info.getValue(),
|
|
436
|
-
}),
|
|
437
|
-
];
|
|
87
|
+
```bash
|
|
88
|
+
npx ctx7 skills install /sergeyshmakov/data-path data-path
|
|
438
89
|
```
|
|
439
90
|
|
|
440
|
-
##
|
|
91
|
+
## Contributing
|
|
441
92
|
|
|
442
|
-
|
|
93
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
443
94
|
|
|
444
|
-
##
|
|
95
|
+
## License
|
|
445
96
|
|
|
446
|
-
|
|
97
|
+
[MIT License](LICENSE)
|