Types and Type Safety
Relevant source files
The following files were used as context for generating this wiki page:
The data-path library is built on a foundation of strict TypeScript type safety, designed to eliminate the common pitfalls of string-based object property access. By leveraging Proxy-based lambda expressions, the system ensures that every path constructed is verified against the underlying data structure at compile-time. This approach provides developers with IDE autocomplete, immediate feedback on refactoring errors, and a robust API for immutable data manipulation.
The core philosophy centers on "Type-Safe Traversal," where the library distinguishes between concrete paths (pointing to a single location) and template paths (using wildcards to match multiple locations). This distinction is enforced through a sophisticated type system that conditionally enables or disables methods based on the resolved type of the data at any given path segment.
Sources: README.md:9-25, src/types.ts:149-166
Core Type Architecture
The system's architecture relies on a hierarchy of interfaces and types that define how paths are constructed, validated, and used. The primary interface, BasePath, provides the shared logic for all path objects, while specialized types like Path and TemplatePath extend this functionality with context-aware methods.
The Path Hierarchy
The diagram above illustrates the relationship between the base path definitions and the specialized path types.
Sources: src/types.ts:111-236, src/impl/path-impl.ts:25-30
Key Data Structures
The following table summarizes the primary types used to maintain type safety across the library:
| Type | Description | File Reference |
|---|---|---|
Segment | Union of string | number representing a key or array index. | src/types.ts:9 |
PathExpression<T, R> | A lambda function (proxy: T) => R used to capture path segments via Proxy. | src/types.ts:68 |
ResolvablePath<T, V> | A union type allowing methods to accept Path objects, raw segments, or lambdas. | src/types.ts:79-83 |
Primitive | Types that terminate traversal (string, number, boolean, etc.). | src/types.ts:58-65 |
CollectionItem<V> | Helper to infer the item type of an array or record during .each() calls. | src/types.ts:47-53 |
Type-Safe Traversal and Conditional Methods
One of the most advanced features of the data-path type system is the conditional exclusion of traversal methods. If a path resolves to a Primitive type, the TypeScript compiler prevents the use of .each() or .deep(), as these operations are logically invalid on leaf nodes.
Traversal Logic Flow
The system uses intersection types to merge BasePath with TraversablePathMethods only when the resolved value is not a primitive.
Sources: src/types.ts:161-166, src/tests/types.test-d.ts:133-138
Implementation Details: Path vs TemplatePath
Standard Path objects are designed for 1:1 mapping. When a wildcard is introduced via .each() or .deep(), the type transitions to a TemplatePath.
Path<T, V, P>: Used for concrete locations..get(data)returns a single value of typeV.TemplatePath<T, V, P>: Used for wildcard locations..get(data)is overloaded to return an arrayV[], and it adds the.expand(data)method to resolve wildcards into concretePath[]objects.
Sources: src/types.ts:161-236, src/impl/template-path-impl.ts:11-30
Proxy-Based Path Capture
The mechanism for generating paths without string literals relies on createPathProxy. This utility creates a recursive Proxy object that records every property access or array index access.
The lambda expression provided to the path() constructor is executed exactly once with this proxy to extract the segments array.
Sources: src/utils.ts:30-45, src/impl/path-impl.ts:241-248
Relational Algebra and Match Types
data-path provides a set of relational methods to compare paths. These methods return a MatchResult, which includes a MatchRelation literal.
Match Relations Table
| Relation | Meaning | Example |
|---|---|---|
equals | Paths are identical segment-for-segment. | a.b == a.b |
parent | This path is a prefix of the other. | a is parent of a.b |
child | The other path is a prefix of this one. | a.b is child of a |
includes | This path (usually a template) covers the other. | a.* includes a.0 |
included-by | The other path (template) covers this one. | a.0 included-by a.* |
Sources: src/types.ts:14-25, src/impl/path-impl.ts:186-210, src/tests/interactions.spec.ts:67-78
Summary of Type Safety Benefits
The type system in data-path ensures that:
- Invalid Paths fail at Compile-Time: Attempting to access
p.nmaeinstead ofp.nameresults in a TypeScript error. - Resolved Types are Known: Using
.get()on aPath<Root, string>correctly infers the return type asstring. - Immutable Updates are Safe: The
.set()method requires the new value to match the typeVdefined during path creation. - Refactoring is Automated: Renaming a property in the interface will trigger TypeScript errors on all associated
path()declarations, unlike string-based alternatives.