|
| 1 | +--- |
| 2 | +name: data-path |
| 3 | +description: Creates and manipulates type-safe object property paths using TypeScript lambda expressions. Use when the user needs typed paths for nested forms (React Hook Form, TanStack Form), state updates (Zustand, useState), validation mapping (Zod), or column accessors (TanStack Table). Use when replacing string paths like "users.0.name" with type-safe alternatives. Don't use for lodash.get, JSONPath, or simple one-level property access. |
| 4 | +--- |
| 5 | + |
| 6 | +# `data-path` skill |
| 7 | + |
| 8 | +Provides type-safe object property paths via Proxy-based lambda expressions. Paths are inferred from TypeScript types and support get/set, merge/subtract, templates (wildcards), and relational algebra. |
| 9 | + |
| 10 | +## When to Apply |
| 11 | + |
| 12 | +Trigger when the user: |
| 13 | +- Binds form fields to nested structures (e.g. `users[i].firstName`) |
| 14 | +- Updates deeply nested state immutably |
| 15 | +- Maps validation errors to specific fields |
| 16 | +- Defines table column accessors |
| 17 | +- Compares or composes paths programmatically |
| 18 | + |
| 19 | +Do not trigger for: |
| 20 | +- Simple `obj.prop` access |
| 21 | +- lodash.get / lodash.set usage |
| 22 | +- JSONPath or XPath-style queries |
| 23 | +- Non-TypeScript projects |
| 24 | + |
| 25 | +## Quick Start |
| 26 | + |
| 27 | +1. Ensure `data-path` is installed: `npm install data-path` |
| 28 | +2. Import: `import { path, unsafePath } from "data-path"` |
| 29 | +3. Define a path with a lambda: `path<RootType>(p => p.nested.field)` |
| 30 | +4. Use `path.$` for string form (e.g. `register(path.$)`), `path.get(data)` to read, `path.set(data, value)` to write |
| 31 | + |
| 32 | +## Step-by-Step Workflows |
| 33 | + |
| 34 | +### Workflow 1: Form Field Binding |
| 35 | + |
| 36 | +When binding a nested form field (React Hook Form, TanStack Form): |
| 37 | + |
| 38 | +1. Define the form value type (e.g. `FormValues`). |
| 39 | +2. Create the path with the loop index in the lambda: `path<FormValues>(p => p.users[i].firstName)`. |
| 40 | +3. Pass `path.$` to `register()` (React Hook Form) or `name` prop (TanStack Form). |
| 41 | +4. Do not create the path outside the map callback—the index `i` must be captured inside the lambda. |
| 42 | + |
| 43 | +### Workflow 2: Immutable State Update |
| 44 | + |
| 45 | +When updating nested state (Zustand, useState): |
| 46 | + |
| 47 | +1. Create the path once at module scope: `const themePath = path<State>(p => p.settings.profile.theme)`. |
| 48 | +2. In the setter: `set(state => themePath.set(state, newValue))`. |
| 49 | +3. `path.set()` returns a structural clone; no manual spreading required. |
| 50 | + |
| 51 | +### Workflow 3: Validation Error Mapping (Zod) |
| 52 | + |
| 53 | +When mapping Zod errors to UI fields: |
| 54 | + |
| 55 | +1. Create the expected path: `path<FormData>(p => p.user.age)`. |
| 56 | +2. For each `issue` in `result.error.issues`, build a path from `issue.path`: `unsafePath<FormData>(issue.path.join("."))`. |
| 57 | +3. Compare: `if (errorPath.equals(agePath)) { /* show error */ }`. |
| 58 | + |
| 59 | +### Workflow 4: Bulk Operations (Templates) |
| 60 | + |
| 61 | +When operating on all items in a collection: |
| 62 | + |
| 63 | +1. Create a template: `path<Data>(p => p.users).each(u => u.name)`. |
| 64 | +2. `path.$` becomes `"users.*.name"`. |
| 65 | +3. Use `templatePath.get(data)` → array of values; `templatePath.set(data, value)` → updates all matches. |
| 66 | +4. Use `templatePath.expand(data)` to get concrete paths for each match. |
| 67 | + |
| 68 | +### Workflow 5: Composing Paths (Reusable Components) |
| 69 | + |
| 70 | +When a component receives a base path and needs to extend it: |
| 71 | + |
| 72 | +1. Base path: `employeePath = path<Company>(p => p.departments[0].employees[5])`. |
| 73 | +2. Sub-path: `nameSubPath = path<Employee>(p => p.profile.firstName)`. |
| 74 | +3. Merge: `absolutePath = employeePath.merge(nameSubPath)`. |
| 75 | +4. Subtract for relative: `relative = absolutePath.subtract(employeePath)`. |
| 76 | + |
| 77 | +## Key Rules |
| 78 | + |
| 79 | +- **Lambda captures at creation time**: Use `path<T>(p => p.users[i].name)` with `i` from the enclosing scope. The path is built once when the lambda runs. |
| 80 | +- **Use `unsafePath` only for dynamic strings**: e.g. from `issue.path.join(".")` or API responses. Prefer `path()` for static structure. |
| 81 | +- **`.get()` returns `undefined`** if any intermediate segment is missing; it does not throw. |
| 82 | +- **`.set()` is immutable**: Returns a new object. Use with functional updaters. |
| 83 | +- **`.each()` and `.deep()`** require a non-primitive value at the path; they are not available on paths ending in string/number/etc. |
| 84 | + |
| 85 | +## API Reference |
| 86 | + |
| 87 | +For the full API cheatsheet (creation, properties, data access, traversal, manipulation, relational), see [references/api.md](references/api.md). |
| 88 | + |
| 89 | +## Common Integrations |
| 90 | + |
| 91 | +| Integration | Use `path.$` for | Use `path.get`/`path.set` for | |
| 92 | +|------------------|------------------|-------------------------------| |
| 93 | +| React Hook Form | `register(name)` | — | |
| 94 | +| TanStack Form | `Field name` | — | |
| 95 | +| Zustand | — | `set(state => path.set(state, v))` | |
| 96 | +| useState | — | `setState(prev => path.set(prev, v))` | |
| 97 | +| TanStack Table | `id` in accessor | `accessor: path.fn` | |
| 98 | +| Zod | — | `unsafePath(issue.path.join(".")).equals(path)` | |
0 commit comments