FieldPath.ts
Overview
export namespace FieldPath
FieldPath is the core abstraction behind form field management in the library.
It provides a type-safe, runtime-aware path system for reading, writing, modifying, and deleting deeply nested values inside complex objects — including arrays.
At runtime, it handles:
- Converting between
["user", "address", 0, "city"]and"user.address[0].city" - Safely walking object trees (with auto-creation of missing branches)
- Getting, setting, modifying, and deleting deeply nested values
- Comparing and matching hierarchical paths
At the type level, it provides:
- Fully inferred string paths ("user.address0.city")
- Compile-time validation of valid object paths
- Deep type resolution (
FieldPath.Resolve<TObject, TPath>) - Smart array index handling with IntelliSense support
In short, FieldPath ensures that form fields remain structurally safe, deeply typed, and predictable, whether you interact with them using string paths or segment arrays.
It acts as the backbone that keeps form state manipulation both ergonomic and strongly typed.
Type Aliases
📗 Segments
export type Segments = readonly PropertyKey[];
Segments is the canonical, low-level representation of an object path.
Each entry corresponds to one navigation step inside an object or array.
It supports:
string→ object keysnumber→ array indicecssymbol→ symbol keys
This format is what all runtime operations (getValue(), setValue(), walkPath(), etc.) operate on internally.
Examples
const path: FieldPath.Segments = ["user", "addresses", 0, "city"];
// ^ Represents obj.user.addresses[0].city
const path: FieldPath.Segments = ["grid". 10, 20, "coord", "x"]
// ^ Represents obj.grid[10][20].coord.x
const META = Symbol("meta");
const path: FieldPath.Segments = ["user", META, "createdAt"];
// ^ Represents: obj.user[META].createdAt
📗 StringPath
export type StringPath = string;
StringPath is the string representation of a field path.
It encodes deep object access using dot notation and bracket indices.
This is the ergonomic, user-facing format designed for APIs and declarative bindings — internally,
it is parsed and normalized into Segments before any operation is performed.
Format Rules
.separates object properties[number]represents array indices- Nested structures combine both
Examples
const path: FieldPath.StringPath = "user.addresses[0].city";
// ^ Equivalent to: ["user", "addresses", 0, "city"]
const path: FieldPath.StringPath = "grid[10][20].coord.x";
// ^ Equivalent to: ["grid", 10, 20, "coord", "x"]
StringPath values can be converted to Segments using FieldPath.fromStringPath(),
and Segments can be serialized back using FieldPath.toStringPath().TObj extends object can be extracted via FieldPath.StringPaths<TObj>,
enabling fully type-safe deep field references.Type Helpers
📗 Resolve<>
export type Resolve<
TObject,
TPath extends FieldPath.Segments,
>;
Resolve<> computes the exact value type located at a given path.
Given an object type and a FieldPath.Segments path, it recursively walks the type and produces the type that exists at the end of that path — including proper array index resolution and nullability handling.
It is the foundation that makes getValue(), setValue(), and modifyValue() fully type-safe.
Resolve<> is what bridges a structural path (FieldPath.Segments) with precise static type inference.
Examples
interface Model {
user: {
addresses: { city: string }[];
};
}
type User = FieldPath.Resolve<Model, ["user"]>;
// ^? { addresses: { city:string }[] }
type Addresses = FieldPath.Resolve<Model, ["user", "addresses"]>;
// ^? { city:string }[]
type Address = FieldPath.Resolve<Model, ["user", "addresses", 0]>;
// ^? { city:string }
type Address = FieldPath.Resolve<Model, ["user", "addresses", 99]>;
// ^? { city:string }
type Address = FieldPath.Resolve<Model, ["user", "addresses", number]>;
// ^? { city:string }
type City = FieldPath.Resolve<Model, ["user", "addresses", 0, "city"]>;
// ^? string
type Nonexistent = FieldPath.Resolve<
Model,
["invalid", "or", "missing", "field"]
>;
// ^? never
const META = Symbol("meta");
type META = typeof META;
interface Model {
[META]: { createdAt: Date };
}
type Meta = FieldPath.Resolve<Model, [META]>;
// ^? { createdAt: Date }
never, preventing invalid deep access at compile time.📗 StringPaths<>
export type StringPaths<TObject extends object>;
StringPaths<> generates the complete union of all valid string paths for a given object type TObject.
It statically analyzes the structure of TObject and produces dot/bracket-notation paths that accurately reflect:
- Nested objects
- Arrays (with index support)
- Deeply composed structures
This enables strict IntelliSense auto-complete and compile-time validation for string-based field access.
Examples
type Model = {
user: {
name: string;
addresses: { city: string }[];
};
};
type ModelPath = FieldPath.StringPaths<Model>;
// ^ This will yield an union identical to this:
"user" |
"user.name" |
"user.addresses" |
"user.addresses[0]" |
`user.addresses[${number}]` |
"user.addresses[0].city" |
`user.addresses[${number}].city`;
const paths: ModelPath[] = [
"user",
"user.name",
"user.addresses[42]",
"user.addresses[0].city",
"user.addresses[99].city",
// ^ ✅ Compiler is happy with these
"user.invalid",
// ^ ❌ Type error
];
[0] is suggested.
However, accessing an array at any index is considered a valid string path.
📗 ParseStringPath<>
export type ParseStringPath<TStrPath extends string>;
ParseStringPath<> converts a string path literal into its exact FieldPath.Segments tuple type at compile time.
It interprets dot notation and bracket indices, producing a strongly typed path representation that can be consumed by Resolve<> and other FieldPath.Segments-based utilities.
This type is what bridges ergonomic string literals with the internal FieldPath.Segments system
— enabling full compile-time validation and deep type inference when working with string-based field paths.
Examples
type Path = FieldPath.ParseStringPath<"user.addresses[0].city">;
// ^? ["user", "addresses", 0, "city"]
type IndexPath = FieldPath.ParseStringPath<"grid[10][20].x">;
// ^? ["grid", 10, 20, "x"]
Runtime Helpers
📙 toStringPath()
export function toStringPath(path: FieldPath.Segments): FieldPath.StringPath;
Converts a Segments path into its canonical string representation.
It serializes object keys using dot notation and array indices using bracket notation — producing a normalized StringPath suitable for display, logging, storage, or public APIs.
Behavior:
stringkeys →"user.name"numberindices →"items[0]"- Nested structures combine both forms
- Output is always normalized
Returns
FieldPath.StringPath;
Arguments
StringPathExamples
const segments: FieldPath.Segments = ["user", "addresses", 0, "city"];
const path = FieldPath.toStringPath(segments);
// ^? "user.addresses[0].city"
const path = FieldPath.toStringPath(["grid", 10, 20, "x"]);
// ^? "grid[10][20].x"
📙 fromStringPath()
export function fromStringPath<TStrPath extends string>(
stringPath: TStrPath,
): FieldPath.ParseStringPath<TStrPath>;
- TStrPath
StringPathto be converted toSegments
Parses a string-based path (StringPath) into its structural Segments representation.
It understands dot notation and bracket indices, producing a normalized array of property keys that can be used directly with getValue(), setValue(), walkPath(), and other core utilities.
When called with a string literal, the return type is inferred precisely using ParseStringPath<>.
Returns
FieldPath.ParseStringPath<TStrPath>;
- TStrPathfrom
fromStringPath()'s generics (See fromStringPath())
Arguments
SegmentsExamples
const path = FieldPath.fromStringPath("user.addresses[0].city");
// ^? ["user", "addresses", 0, "city"]
const path = FieldPath.fromStringPath("grid[10][20].x");
// ^? ["grid", 10, 20, "x"]
const path = FieldPath.fromStringPath("users[abc]");
// ^? ["users", "abc"]
📙 equals()
export function equals(
path1?: FieldPath.Segments,
path2?: FieldPath.Segments,
): boolean;
Performs a strict, segment-by-segment comparison between two paths.
Returns true only if both paths:
- Are defined
- Have the same length
- Contain identical segments in the same order
No normalization or coercion is performed — comparison is structural and exact.
Returns
boolean;
Arguments
path1 exactly (length and segments) for the result to be true.Examples
FieldPath.equals(
["user", "address", 0, "city"],
["user", "address", 0, "city"],
); // => true
FieldPath.equals(
["user", "address", 0, "city"],
["user", "address", 1, "city"],
); // => false
FieldPath.equals(
["world", "tiles", 100, 200],
["world", "tiles", 100, 200, "blockId"],
); // => false
📙 isDescendant()
export function isDescendant(
parentPath: FieldPath.Segments,
childPath: FieldPath.Segments,
): boolean;
Determines whether childPath is structurally nested under parentPath.
Returns true only if:
childPathis strictly longer thanparentPath, and- Every segment of
parentPathmatches the beginning ofchildPath.
This is a prefix check — not a deep comparison.
Returns
boolean;
Arguments
parentPath and be strictly longer to qualify as a descendant.Examples
FieldPath.isDescendant(
["user", "detail", "addresses"],
["user", "detail", "addresses", 0, "city"],
); // => true
FieldPath.isDescendant(
["user", "detail", "addresses"],
["user", "detail", "profile"],
); // => false
FieldPath.isDescendant(
["user", "addresses", 0, "city"],
["user", "addresses", 0, "city"],
); // => false
📙 walkPath()
export function walkPath<
TObject extends object,
const TPath extends FieldPath.Segments,
>(
object: TObject,
path: TPath,
opts?: { returnOnEmptyBranch?: boolean },
): {
target: any;
key: PropertyKey | null;
};
- TObjectThe root object type that will be traversed and potentially mutated during path walking.
- TPathA segmented path describing where traversal should occur within
TObject. Its structure dictates how branches are navigated (and created, if necessary).
Traverses object up to the parent container of the final segment in TPath,
then returns the container and the last key.
It is the internal primitive powering setValue(), modifyValue(), and deleteValue().
By default, missing intermediate branches are created automatically (object or array depending on the upcoming segment).
Returns
{
target: any;
key: PropertyKey | null;
}
- targetThe object that directly contains the final property.
- keyThe last segment of
TPath. It will benullonly whenreturnOnEmptyBranchis enabled and traversal stops early.
Arguments
true, traversal stops and returns { target: null, key: null } instead of creating missing branches.Examples
const obj = {};
const path = ["user", "addresses", 0, "city"];
const { target, key } = FieldPath.walkPath(obj, path);
// ^? "city"
target[key] = "Berlin";
console.log(obj);
// {
// user: {
// addresses: [
// { city: "Berlin" }
// ]
// }
// }
const obj = {};
const path = ["user", "name"];
const result = FieldPath.walkPath(obj, path, {
returnOnEmptyBranch: true,
});
console.log(result);
// { target: null, key: null }
📙 getValue()
export function getValue<
TObject extends object,
const TPath extends FieldPath.Segments,
>(object: TObject, path: TPath): FieldPath.Resolve<TObject, TPath> | undefined;
- TObjectThe root object type being traversed. All type inference and deep resolution are computed relative to this structure.
- TPathA segmented path pointing to a nested property inside
TObject. Its structure determines both the runtime traversal and the inferred return type viaFieldPath.Resolve<TObject, TPath>.
Reads the value located at TPath inside TObject.
The return type is fully inferred using FieldPath.Resolve<>.
null or undefined, the function immediately returns undefined instead of throwing.Returns
FieldPath.Resolve<TObject, TPath> | undefined;
- TObject*from
getValue()'s generics (See getValue()) - TPath*from
getValue()'s generics (See getValue()) - FieldPath.Resolve<TOutput, TPath>value type of
TOutputat givenTPath
Arguments
Examples
const data = {
user: {
addresses: [{ city: "Berlin" }],
},
};
const path = ["user", "addresses", 0, "city"];
const city = FieldPath.getValue(data, path);
// city inferred as: string | undefined
// value: "Berlin"
const data = {};
const path = ["user", "addresses", 0, "city"];
const city = FieldPath.getValue(data, path);
// city === undefined
// no runtime error
📙 setValue()
export function setValue<
TObject extends object,
const TPath extends FieldPath.Segments,
>(object: TObject, path: TPath, value: FieldPath.Resolve<TObject, TPath>): void;
- TObjectThe root object type being traversed. Defines the full structural context in which the path will be resolved and mutated.
- TPathA segmented path. Determines both the target location inside
TObjectand the expected value type viaFieldPath.Resolve<TObject, TPath>.
Writes value to object at the location described by TPath.
Missing intermediate branches are created automatically, ensuring the assignment always lands at the correct depth.
The value type is strictly inferred using FieldPath.Resolve<>, preventing invalid assignments at compile time.
Arguments
TObject at TPath.Examples
const data = {};
const path = ["user", "addresses", 0, "city"];
FieldPath.setValue(data, path, "Berlin");
console.log(data);
// {
// user: {
// addresses: [
// { city: "Berlin" }
// ]
// }
// }
type Model = {
user: { age: number };
};
const model: Model = { user: { age: 18 } };
FieldPath.setValue(model, ["user", "age"], 25); // ✅
FieldPath.setValue(model, ["user", "age"], "25"); // ❌ Type error
📙 modifyValue()
export function modifyValue<
TObject extends object,
const TPath extends FieldPath.Segments,
>(
object: TObject,
path: TPath,
modifier: (
currentValue: FieldPath.Resolve<TObject, TPath> | undefined,
) => void,
): void;
- TObjectThe root object type whose structure defines where the modification occurs. All type inference for the current value is derived from this shape.
- TPathA segmented path describing the exact location inside
TObjectto modify. It determines the type ofcurrentValuethroughFieldPath.Resolve<TObject, TPath>.
Executes a mutation function against the value located at TPath.
Unlike setValue(), this does not replace the value directly.
Instead, it provides the current value to a modifier callback, allowing in-place updates (e.g. pushing into arrays or mutating objects).
Missing branches are created automatically before the modifier runs.
Arguments
Examples
const data = {
user: {
tags: [] as string[],
},
};
const path = ["user", "tags"];
FieldPath.modifyValue(data, path, (tags) => {
// ^? string[] | undefined
tags?.push("admin");
});
console.log(data.user.tags);
// ["admin"]
const data = {};
const path = ["user", "count"];
FieldPath.modifyValue(data, path, (count) => {
// ^? undefined
});
📙 deleteValue()
export function deleteValue<
TObject extends object,
TPath extends readonly PropertyKey[],
>(object: TObject, path: TPath): void;
- TObjectThe root object type being traversed. Defines the structural boundary within which the deletion is evaluated.
- TPathA segmented path pointing to the property inside
TObjectthat should be removed. Its structure determines which branch is walked at runtime.
Removes the property located at TPath from object.
Traversal is performed safely. If any intermediate branch does not exist, the operation exits without mutation.
Unlike setValue() or modifyValue(), this does not create missing branches.
Arguments
Examples
const data = {
user: {
name: "John",
age: 25,
},
};
FieldPath.deleteValue(data, ["user", "age"]);
console.log(data);
// {
// user: {
// name: "John"
// }
// }
const data = {};
FieldPath.deleteValue(data, ["user", "age"]);
// No error, no mutation