Introduction
Relevant source files
The following files were used as context for generating this wiki page:
data-path is a modern, zero-dependency TypeScript library designed to solve the inherent risks of string-based object property paths (e.g., "users.0.name"). In standard development, string paths lack type safety, break during refactoring, and offer no IDE autocomplete. This library replaces opaque strings with Proxy-based lambda expressions, providing 100% type safety and a rich API for building, comparing, and manipulating paths.
Sources: README.md:1-25, skills/data-path/SKILL.md:1-10
The library is stack-agnostic and particularly suited for deep state management (Zustand, React useState), form field binding (React Hook Form, TanStack Form), and data validation mapping (Zod). It treats paths as first-class objects capable of relational algebra, such as determining if one path is a child of another or merging two paths with intelligent deduplication.
Sources: README.md:50-60, README.md:180-200, skills/data-path/SKILL.md:65-80
Core Architecture and Path Discovery
The fundamental mechanism of data-path relies on JavaScript Proxies to "record" property access during the execution of a lambda expression. When a user provides a function like p => p.profile.firstName, the library passes a Proxy into that function.
Proxy-Based Path Capture
The createPathProxy utility generates a recursive Proxy. Every property access on this Proxy returns a new Proxy that appends the accessed key to an internal PATH_SEGMENTS array. This allows the library to transform a type-safe TypeScript expression into a concrete list of segments (strings for object keys or numbers for array indices).
The diagram shows how property access on a Proxy chain builds the segment list used to instantiate a Path object.
Sources: src/utils.ts:27-43, src/impl/path-impl.ts:245-251
Handling Indices and Special Keys
The library distinguishes between canonical array indices and standard object keys. Numeric-looking keys (e.g., "0", "123") are converted to numbers to ensure they match array indices at runtime, while non-canonical strings (e.g., "01", "1e3") are preserved as strings.
Sources: src/utils.ts:8-16, src/tests/data-access.spec.ts:133-148
Primary Components
The system is structured around three main interfaces that define how paths behave based on their content and target values.
| Component | Description |
|---|---|
BasePath | The foundation for all paths, containing segments, string serialization, and relational methods (equals, match). |
Path | A standard path representing a single location in an object. Inherits from BasePath. |
TemplatePath | A path containing wildcards (* or **) used for bulk operations on collections or deep structures. |
Sources: src/types.ts:131-158, src/types.ts:219-225, src/types.ts:241-250
Class Hierarchy
Standard paths and template paths share common logic through implementation classes, but differ in how they handle data access and traversal.
This diagram represents the relationship between the core interfaces and their concrete implementations.
Sources: src/impl/path-impl.ts:24-30, src/impl/template-path-impl.ts:13-17
Data Interaction Logic
data-path provides two primary methods for interacting with data: .get() for reading and .set() for immutable writing.
Safe Data Retrieval (.get)
The .get(data) method traverses the provided object using the stored segments. It is designed for safety; if any intermediate property is null or undefined, the method returns undefined immediately rather than throwing a runtime error.
Sources: src/impl/path-impl.ts:72-80, README.md:40-45
Immutable Updates (.set)
The .set(data, value) method performs a structural clone of the object. It only recreates the references along the path being updated, allowing for efficient structural sharing. If intermediate paths do not exist, the library automatically creates objects or arrays (based on segment type) to complete the path.
Sources: src/impl/path-impl.ts:98-138, src/tests/data-access.spec.ts:89-110
Templates and Wildcards
Templates allow for "bulk" operations by introducing wildcards into the path segments.
Wildcard Types
- Single-Level Wildcard (
*): Created via.each(). It matches all keys in a record or all indices in an array. - Deep Wildcard (
**): Created via.deep(). It matches a property at any depth within the tree.
Sources: src/types.ts:252-270, src/impl/template-path-impl.ts:18-65
Template Expansion
The .expand(data) method is used to resolve a template path into an array of concrete Path objects based on the actual structure of a data object. This is useful for finding every specific path that matches a pattern.
Sources: src/impl/template-path-impl.ts:73-125, README.md:148-154
Relational Algebra
A key feature of the library is the ability to compare paths programmatically. This is essential for features like permission systems or mapping validation errors to specific fields.
| Relation | Description |
|---|---|
equals | Paths have identical segments. |
startsWith | This path is a child or the same as the prefix path. |
includes | This path is a parent of the other path. |
match | Returns a detailed MatchResult (relation and params). |
Sources: src/types.ts:185-212, src/impl/path-impl.ts:184-220, skills/data-path/references/api.md:45-55
The sequence diagram illustrates the internal flow when comparing two paths for a prefix relationship.
Sources: src/impl/path-impl.ts:184-186, src/utils.ts:68-85
Summary
data-path provides a robust, type-safe alternative to string-based object paths by leveraging TypeScript's type system and JavaScript's Proxy API. By treating paths as structured objects, it enables complex data manipulations, bulk operations via templates, and relational comparisons while maintaining strict immutability and high performance through structural sharing.