Creating Paths
Relevant source files
The following files were used as context for generating this wiki page:
Creating paths is the foundational feature of the data-path library. It allows developers to define type-safe, deep object property paths using TypeScript lambda expressions or raw strings. By leveraging Proxy-based builders, the system captures property accesses to build a serializable and manipulatable representation of data structures without requiring actual data during the definition phase. Sources: README.md, src/path.ts:10-23
Paths can be constructed as "Root" paths, derived from lambdas, or composed by extending existing paths. This flexibility supports various use cases, from form field binding in React to immutable state updates in stores like Zustand. Sources: README.md, src/types.ts:250-261
Path Construction Methods
The library provides two primary functions for path creation: path<T>() and unsafePath<T>(). While path is the recommended type-safe approach, unsafePath provides an escape hatch for dynamic or string-based contexts. Sources: src/path.ts:32-88
Type-Safe Lambda Creation
The path() function uses a Proxy-based builder. When a lambda like (u) => u.profile.name is passed, the internal proxy records every property access as a segment. Sources: src/path.ts:47-53, src/impl/path-impl.ts:40-55
The diagram shows the logic flow inside the path() constructor, distinguishing between root creation, lambda inference, and path extension. Sources: src/path.ts:32-75
Construction Varieties
| Method | Syntax | Use Case |
|---|---|---|
| Root Path | path<T>() | Creating a starting point for a known type. |
| Lambda Path | path<T>(p => p.a.b) | Defining a specific nested path with IDE autocomplete. |
| Extended Path | path(base, p => p.c) | Appending segments to an existing path object. |
| Unsafe Path | unsafePath<T>("a.b") | Converting dynamic strings (e.g., from API) into path objects. |
Sources: src/path.ts:32-44, src/types.ts:250-264
Internal Architecture
The creation process relies on the PathImpl class and a Proxy mechanism. When a path is created via lambda, the createPathProxy utility (internal) tracks property access. Sources: src/impl/path-impl.ts:24-35, src/path.ts:48-52
The Recording Mechanism
- Proxy Initialization: A proxy is created with an empty segments array.
- Access Capture: As the lambda accesses properties (e.g.,
p.items[0]), the proxy records strings ("items") and numbers (0). - Segment Extraction: The resulting object from the lambda contains a hidden symbol
PATH_SEGMENTSwhichPathImpluses to initialize its internal state.
Sources: src/path.ts:48-53, src/impl/path-impl.ts:180-185, src/tests/path.spec.ts:15-28
Handling Edge Cases in Creation
The creation logic handles several JavaScript-specific scenarios during the lambda execution:
- Dynamic Indices: Captures runtime variables used as array indices. Sources: src/tests/path.spec.ts:21-25
- Conditional Branching: Only records the path taken during the single execution of the lambda. Sources: src/tests/path.spec.ts:38-44
- Literal Returns: If the lambda returns a literal or
null, an empty path is produced. Sources: src/tests/path.spec.ts:68-75
Unsafe Path Creation
The unsafePath function is used to bridge the gap between string-based systems and the type-safe path objects. It performs "best-effort" parsing of dot-notation strings. Sources: src/path.ts:85-93
export function unsafePath<T>(raw: string): Path<T, unknown, string> {
const segments: Segment[] = raw
? raw
.split(".")
.map((s) => (s === "" ? s : isCanonicalArrayIndex(s) ? Number(s) : s))
: [];
return new PathImpl<T, unknown, string>(segments);
}
Sources: src/path.ts:85-93
Parsing Rules for Unsafe Paths:
- Dots: Split by
.to create segments. - Numbers: If a segment represents a "canonical array index" (like "0" or "10"), it is converted to a
numbertype. Sources: src/path.ts:89-91, src/tests/unsafe.spec.ts:25-28 - Malformed Strings: Leading dots or multiple dots result in empty string segments (
""). Sources: src/tests/unsafe.spec.ts:37-45
Path Composition and Extension
Paths can be extended after creation using the .to() method or the path(base, expr) overload. This is critical for building reusable components that work on sub-sections of a data tree. Sources: src/impl/path-impl.ts:257-263, src/types.ts:233-239
The sequence shows how the .to() method acts as a wrapper around the core construction logic to facilitate path chaining. Sources: src/impl/path-impl.ts:257-263
Conclusion
Creating paths in data-path is designed to be a side-effect-free operation that translates developer intent (via lambdas or strings) into a structured PathImpl object. By supporting both strict type-safe lambdas and flexible unsafe strings, the system ensures that paths can be defined once and reused across data access, state manipulation, and relational algebra operations. Sources: src/path.ts:10-30, src/impl/path-impl.ts:24-40