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 +60 -407
- 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 +109 -68
- package/dist/index.d.mts +0 -319
- package/dist/index.mjs +0 -499
package/README.md
CHANGED
|
@@ -1,444 +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
|
-
|
|
31
|
+
## What it is
|
|
59
32
|
|
|
60
|
-
|
|
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
|
-
|
|
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
|
-
|
|
41
|
+
## Install
|
|
65
42
|
|
|
66
43
|
```bash
|
|
67
|
-
|
|
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
|
-
|
|
47
|
+
Requirements: Node `>=20`, TypeScript `>=5.0`
|
|
367
48
|
|
|
368
|
-
|
|
49
|
+
## Quick start
|
|
369
50
|
|
|
370
|
-
```
|
|
371
|
-
import { useState } from "react";
|
|
51
|
+
```ts
|
|
372
52
|
import { path } from "data-path";
|
|
373
53
|
|
|
374
|
-
type
|
|
375
|
-
|
|
376
|
-
const themePath = path<AppState>(p => p.settings.profile.theme);
|
|
54
|
+
type User = { profile: { firstName: string; lastName: string }; tags: string[] };
|
|
377
55
|
|
|
378
|
-
|
|
379
|
-
const [state, setState] = useState<AppState>({
|
|
380
|
-
settings: { profile: { theme: "light" } },
|
|
381
|
-
});
|
|
56
|
+
const firstNamePath = path((u: User) => u.profile.firstName);
|
|
382
57
|
|
|
383
|
-
|
|
384
|
-
|
|
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
|
-
|
|
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
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
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
|
-
|
|
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
|
-
|
|
421
|
-
|
|
422
|
-
|
|
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
|
-
|
|
83
|
+
## AI tooling
|
|
425
84
|
|
|
426
|
-
|
|
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
|
-
|
|
430
|
-
|
|
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
|
-
##
|
|
91
|
+
## Contributing
|
|
439
92
|
|
|
440
|
-
|
|
93
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
441
94
|
|
|
442
|
-
##
|
|
95
|
+
## License
|
|
443
96
|
|
|
444
|
-
|
|
97
|
+
[MIT License](LICENSE)
|