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 +22 -0
- package/README.md +377 -0
- package/dist/index.d.mts +319 -0
- package/dist/index.d.ts +319 -0
- package/dist/index.js +527 -0
- package/dist/index.mjs +499 -0
- package/package.json +75 -0
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
|
+
[](https://badge.fury.io/js/data-path)
|
|
6
|
+
[](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).
|