data-path 1.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 ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sergei Shmakov
4
+ https://github.com/sergeyshmakov
5
+
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:
12
+
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 ADDED
@@ -0,0 +1,377 @@
1
+ # data-path
2
+
3
+ A simple, modern, zero-dependency, and fully type-safe library for building, comparing, and manipulating object property paths using TypeScript lambda expressions.
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)
7
+
8
+ ## The Problem
9
+
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:
11
+
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.
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"
50
+ ```
51
+
52
+ ## Installation
53
+
54
+ ```bash
55
+ npm install data-path
56
+ ```
57
+
58
+ _(or use `yarn add data-path`, `pnpm add data-path`, `bun add data-path`)_
59
+
60
+ ## Philosophy
61
+
62
+ - **Stack Agnostic:** Pure data manipulation. Works perfectly with React, Vue, Node.js, or vanilla JavaScript.
63
+ - **Zero Dependencies:** A tiny, efficient footprint that doesn't bloat your bundle.
64
+ - **Fully Type-Safe:** Built strictly for TypeScript. If the structure changes, the compiler will instantly catch broken paths.
65
+ - **Immutable:** All `.set()` operations return structurally cloned objects, making it the perfect companion for modern state managers (Redux, Zustand) and reactive frameworks.
66
+
67
+ ## Core API
68
+
69
+ ### Path Creation
70
+
71
+ You can create paths starting from the root of a type, using a lambda expression, or unsafely from a raw string.
72
+
73
+ ```typescript
74
+ // From root
75
+ const root = path<User>();
76
+ const profile = root.to(p => p.profile);
77
+
78
+ // Direct lambda
79
+ const tagsPath = path<User>(p => p.tags[0]);
80
+
81
+ // From raw string (dynamic contexts)
82
+ import { unsafePath } from "data-path";
83
+ const dynamicPath = unsafePath<User>("profile.firstName");
84
+ ```
85
+
86
+ ### Data Access
87
+
88
+ ```typescript
89
+ const namePath = path<User>(p => p.profile.firstName);
90
+
91
+ // Read
92
+ namePath.get(user); // "Alice"
93
+
94
+ // Accessor shorthand (perfect for arrays)
95
+ const users = [user1, user2];
96
+ const names = users.map(namePath.fn);
97
+
98
+ // Write (Immutable)
99
+ const updated = namePath.set(user, "Alice 2.0");
100
+ ```
101
+
102
+ ### Manipulation
103
+
104
+ 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.
105
+
106
+ ```typescript
107
+ // 1. We have a specific record's path
108
+ const employeePath = path<Company>(p => p.departments[0].employees[5]);
109
+
110
+ // 2. We have a generic sub-path
111
+ const nameSubPath = path<Employee>(p => p.profile.firstName);
112
+
113
+ // Merge them! (Smart deduplication if they overlap)
114
+ const absoluteNamePath = employeePath.merge(nameSubPath);
115
+ console.log(absoluteNamePath.$); // "departments.0.employees.5.profile.firstName"
116
+
117
+ // Subtract a base path to find the relative path
118
+ const relative = absoluteNamePath.subtract(employeePath);
119
+ console.log(relative?.$); // "profile.firstName"
120
+
121
+ // Slice segments (just like Array.prototype.slice)
122
+ const sliced = absoluteNamePath.slice(0, 3);
123
+ console.log(sliced.$); // "departments.0.employees"
124
+ ```
125
+
126
+ ### Templates & Wildcards
127
+
128
+ 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.
129
+
130
+ ```typescript
131
+ type AppData = { users: Array<{ id: string; name: string }> };
132
+ const data: AppData = {
133
+ users: [
134
+ { id: "1", name: "Alice" },
135
+ { id: "2", name: "Bob" },
136
+ ],
137
+ };
138
+
139
+ // 1. Create a template targeting ALL users
140
+ const allUsersPath = path<AppData>(p => p.users).each();
141
+ console.log(allUsersPath.$); // "users.*"
142
+
143
+ // 2. Create a template targeting ALL user names
144
+ const allNamesPath = path<AppData>(p => p.users).each(u => u.name);
145
+ console.log(allNamesPath.$); // "users.*.name"
146
+
147
+ // 3. Bulk Read: extract an array of all matched values
148
+ const names = allNamesPath.get(data);
149
+ console.log(names); // ["Alice", "Bob"]
150
+
151
+ // 4. Bulk Write: immutably update all matches in one go!
152
+ const anonymizedData = allNamesPath.set(data, "Hidden");
153
+ console.log(anonymizedData.users[0].name); // "Hidden"
154
+ console.log(anonymizedData.users[1].name); // "Hidden"
155
+
156
+ // 5. Expand: resolve the template into concrete paths based on actual data
157
+ const concretePaths = allNamesPath.expand(data);
158
+ console.log(concretePaths.map(p => p.$));
159
+ // ["users.0.name", "users.1.name"]
160
+
161
+ // 6. Deep Scan: target a property at any depth
162
+ const deepIds = path<AppData>().deep(node => node.id);
163
+ console.log(deepIds.$); // "**.id"
164
+ ```
165
+
166
+ ### Runtime Variables & Indices
167
+
168
+ 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.
169
+
170
+ ```typescript
171
+ function getUserPropertyPath(userIdx: number, property: "name" | "email") {
172
+ // Capture function arguments directly in the path!
173
+ return path<AppData>(p => p.users[userIdx][property]);
174
+ }
175
+
176
+ const namePath = getUserPropertyPath(2, "name");
177
+ console.log(namePath.$); // "users.2.name"
178
+
179
+ const emailPath = getUserPropertyPath(5, "email");
180
+ console.log(emailPath.$); // "users.5.email"
181
+ ```
182
+
183
+ ### Relational Algebra
184
+
185
+ Path algebra is extremely useful for permissions, validation, and complex UI logic where you need to know how two paths relate to each other.
186
+
187
+ ```typescript
188
+ const userPath = path<User>();
189
+ const profilePath = userPath.to(p => p.profile);
190
+ const namePath = userPath.to(p => p.profile.firstName);
191
+
192
+ // Check relationships
193
+ namePath.startsWith(profilePath); // true
194
+ profilePath.includes(namePath); // true
195
+ namePath.includes(profilePath); // false
196
+ namePath.equals(namePath); // true
197
+
198
+ // .match() provides detailed relational context:
199
+ // returns 'includes', 'included-by', 'equals', 'parent', 'child', or null
200
+ namePath.match(profilePath); // { relation: 'child', params: {} }
201
+ profilePath.match(namePath); // { relation: 'parent', params: {} }
202
+ ```
203
+
204
+ ## Real-World Examples
205
+
206
+ ### 1. React Hook Form
207
+
208
+ 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`).
209
+
210
+ ```tsx
211
+ import { useForm, useFieldArray } from "react-hook-form";
212
+ import { path } from "data-path";
213
+
214
+ type FormValues = { users: Array<{ id: string; firstName: string }> };
215
+
216
+ function UsersForm() {
217
+ const { register, control } = useForm<FormValues>({
218
+ defaultValues: { users: [{ id: "1", firstName: "" }] },
219
+ });
220
+ const { fields } = useFieldArray({ control, name: "users" });
221
+
222
+ return (
223
+ <form>
224
+ {fields.map((field, i) => {
225
+ const namePath = path<FormValues>(p => p.users[i].firstName);
226
+ return (
227
+ <input
228
+ key={field.id}
229
+ {...register(namePath.$)}
230
+ placeholder="First name"
231
+ />
232
+ );
233
+ })}
234
+ </form>
235
+ );
236
+ }
237
+ ```
238
+
239
+ ### 2. TanStack Form
240
+
241
+ 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`.
242
+
243
+ ```tsx
244
+ import { useForm } from "@tanstack/react-form";
245
+ import { path } from "data-path";
246
+
247
+ type FormValues = { users: Array<{ firstName: string }> };
248
+
249
+ function UsersForm() {
250
+ const form = useForm<FormValues>({
251
+ defaultValues: { users: [{ firstName: "" }] },
252
+ onSubmit: ({ value }) => console.log(value),
253
+ });
254
+
255
+ return (
256
+ <form>
257
+ {form.state.values.users.map((_, i) => {
258
+ const namePath = path<FormValues>(p => p.users[i].firstName);
259
+ return (
260
+ <form.Field key={i} name={namePath.$}>
261
+ {field => (
262
+ <input
263
+ name={field.name}
264
+ value={field.state.value}
265
+ onChange={e =>
266
+ field.handleChange(e.target.value)
267
+ }
268
+ />
269
+ )}
270
+ </form.Field>
271
+ );
272
+ })}
273
+ </form>
274
+ );
275
+ }
276
+ ```
277
+
278
+ ### 3. Zustand
279
+
280
+ 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()`.
281
+
282
+ ```typescript
283
+ import { create } from "zustand";
284
+ import { path } from "data-path";
285
+
286
+ type StoreState = {
287
+ settings: { profile: { theme: string } };
288
+ setTheme: (theme: string) => void;
289
+ };
290
+
291
+ const themePath = path<StoreState>(p => p.settings.profile.theme);
292
+
293
+ const useStore = create<StoreState>(set => ({
294
+ settings: { profile: { theme: "light" } },
295
+ setTheme: newTheme => set(state => themePath.set(state, newTheme)),
296
+ }));
297
+ ```
298
+
299
+ ### 4. React `useState`
300
+
301
+ 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.
302
+
303
+ ```tsx
304
+ import { useState } from "react";
305
+ import { path } from "data-path";
306
+
307
+ type AppState = { settings: { profile: { theme: string } } };
308
+
309
+ const themePath = path<AppState>(p => p.settings.profile.theme);
310
+
311
+ function ThemeToggle() {
312
+ const [state, setState] = useState<AppState>({
313
+ settings: { profile: { theme: "light" } },
314
+ });
315
+
316
+ const setTheme = (theme: string) => {
317
+ setState(prev => themePath.set(prev, theme));
318
+ };
319
+
320
+ return <button onClick={() => setTheme("dark")}>{state.settings.profile.theme}</button>;
321
+ }
322
+ ```
323
+
324
+ ### 5. Zod / Validation Mapping
325
+
326
+ 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`.
327
+
328
+ ```typescript
329
+ import { z } from "zod";
330
+ import { unsafePath, path } from "data-path";
331
+
332
+ const schema = z.object({
333
+ user: z.object({ age: z.number().min(18) }),
334
+ });
335
+ type FormData = z.infer<typeof schema>;
336
+ const agePath = path<FormData>(p => p.user.age);
337
+
338
+ const result = schema.safeParse(data);
339
+ if (!result.success) {
340
+ for (const issue of result.error.issues) {
341
+ const errorPath = unsafePath<FormData>(issue.path.join("."));
342
+ if (errorPath.equals(agePath)) {
343
+ console.log("Age must be at least 18!");
344
+ }
345
+ }
346
+ }
347
+ ```
348
+
349
+ ### 6. TanStack Table
350
+
351
+ 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).
352
+
353
+ ```typescript
354
+ import { createColumnHelper } from "@tanstack/react-table";
355
+ import { path } from "data-path";
356
+
357
+ type User = { id: string; contact: { email: string } };
358
+
359
+ const columnHelper = createColumnHelper<User>();
360
+ const emailPath = path<User>(p => p.contact.email);
361
+
362
+ const columns = [
363
+ columnHelper.accessor(emailPath.fn, {
364
+ id: emailPath.$,
365
+ header: "Email",
366
+ cell: info => info.getValue(),
367
+ }),
368
+ ];
369
+ ```
370
+
371
+ ## Contributing
372
+
373
+ We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for more details.
374
+
375
+ ## License
376
+
377
+ This project is licensed under the [ISC License](LICENSE).