@typed-web/form-utils 0.1.1

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Łukasz Jagodziński
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,56 @@
1
+ # @typed-web/form-utils
2
+
3
+ Utilities for form handling: path conversion, object flattening, and array diffing.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @typed-web/form-utils
9
+ ```
10
+
11
+ ## Exports
12
+
13
+ ### Object Conversion
14
+ - `flatObjectToNestedObject` - Convert flat objects to nested structures
15
+ - `nestedObjectToFlatObject` - Convert nested objects to flat structures
16
+
17
+ ### Path Utilities
18
+ - `setValueAtPath` - Set value at path in object
19
+ - `stringPathToArrayPath` - Convert string path to array
20
+ - `arrayPathToStringPath` - Convert array path to string
21
+
22
+ ### Array Utilities
23
+ - `getArraysDiff` - Compute differences between arrays
24
+
25
+ ### Types
26
+ - `FlatObject`, `NestedObject`, `GetArraysDiffArgs`
27
+
28
+ ## Usage
29
+
30
+ ```typescript
31
+ import {
32
+ flatObjectToNestedObject,
33
+ stringPathToArrayPath,
34
+ setValueAtPath,
35
+ } from "@typed-web/form-utils";
36
+
37
+ // Convert flat to nested
38
+ const nested = flatObjectToNestedObject({
39
+ "user.name": "John",
40
+ "addresses[0].city": "NYC",
41
+ });
42
+ // { user: { name: "John" }, addresses: [{ city: "NYC" }] }
43
+
44
+ // Parse path
45
+ const path = stringPathToArrayPath("user.addresses[0].city");
46
+ // ["user", "addresses", 0, "city"]
47
+
48
+ // Set value at path
49
+ const obj = {};
50
+ setValueAtPath(obj, ["user", "name"], "John");
51
+ // { user: { name: "John" } }
52
+ ```
53
+
54
+ ## License
55
+
56
+ MIT
@@ -0,0 +1,9 @@
1
+ export type { FlatObject, NestedObject } from "./lib/types.ts";
2
+ export { setValueAtPath } from "./lib/set-value-at-path.ts";
3
+ export { stringPathToArrayPath } from "./lib/string-path-to-array-path.ts";
4
+ export { arrayPathToStringPath } from "./lib/array-path-to-string-path.ts";
5
+ export { flatObjectToNestedObject } from "./lib/flat-object-to-nested-object.ts";
6
+ export { nestedObjectToFlatObject } from "./lib/nested-object-to-flat-object.ts";
7
+ export { getArraysDiff } from "./lib/get-arrays-diff.ts";
8
+ export type { GetArraysDiffArgs } from "./lib/get-arrays-diff.ts";
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,YAAY,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAG/D,OAAO,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAC5D,OAAO,EAAE,qBAAqB,EAAE,MAAM,oCAAoC,CAAC;AAC3E,OAAO,EAAE,qBAAqB,EAAE,MAAM,oCAAoC,CAAC;AAG3E,OAAO,EAAE,wBAAwB,EAAE,MAAM,uCAAuC,CAAC;AACjF,OAAO,EAAE,wBAAwB,EAAE,MAAM,uCAAuC,CAAC;AAGjF,OAAO,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AACzD,YAAY,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,142 @@
1
+ // src/lib/string-path-to-array-path.ts
2
+ var BRACKET_NOTATION_REGEX = /^\[(.+?)\](.*)$/;
3
+ var DOT_NOTATION_REGEX = /^\.?([^\.\[\]]+)(.*)$/;
4
+ var NUMERIC_KEY_REGEX = /^\d+$/;
5
+ function parsePath(currentPath) {
6
+ if (currentPath.length === 0) {
7
+ return [];
8
+ }
9
+ const bracketMatch = currentPath.match(BRACKET_NOTATION_REGEX);
10
+ const dotMatch = currentPath.match(DOT_NOTATION_REGEX);
11
+ if (bracketMatch) {
12
+ const [, key = "", rest = ""] = bracketMatch;
13
+ const parsedKey = NUMERIC_KEY_REGEX.test(key) ? Number(key) : key;
14
+ const restResult = parsePath(rest);
15
+ if (restResult === null) {
16
+ return null;
17
+ }
18
+ return [parsedKey, ...restResult];
19
+ } else if (dotMatch) {
20
+ const [, key = "", rest = ""] = dotMatch;
21
+ const parsedKey = NUMERIC_KEY_REGEX.test(key) ? Number(key) : key;
22
+ const restResult = parsePath(rest);
23
+ if (restResult === null) {
24
+ return null;
25
+ }
26
+ return [parsedKey, ...restResult];
27
+ }
28
+ return null;
29
+ }
30
+ function stringPathToArrayPath(path) {
31
+ if (path.length === 0) {
32
+ return [];
33
+ }
34
+ const result = parsePath(path);
35
+ if (result === null) {
36
+ return [path];
37
+ }
38
+ return result;
39
+ }
40
+
41
+ // src/lib/set-value-at-path.ts
42
+ function setValueAtPath(object, path, value) {
43
+ const pathSegments = stringPathToArrayPath(path);
44
+ if (pathSegments.length === 0) {
45
+ throw new Error("Cannot set value on empty path");
46
+ }
47
+ const leadingSegments = pathSegments.slice(0, -1);
48
+ const lastSegment = pathSegments[pathSegments.length - 1];
49
+ if (lastSegment === void 0) {
50
+ throw new Error("Invalid path: last segment is undefined");
51
+ }
52
+ let currentObject = object;
53
+ for (let i = 0; i < leadingSegments.length; i++) {
54
+ const currentSegment = leadingSegments[i];
55
+ if (currentSegment === void 0) {
56
+ throw new Error(`Invalid path: segment at index ${i} is undefined`);
57
+ }
58
+ if (!(currentSegment in currentObject)) {
59
+ const nextSegment = leadingSegments[i + 1] ?? lastSegment;
60
+ currentObject[currentSegment] = typeof nextSegment === "number" ? [] : {};
61
+ }
62
+ const nextValue = currentObject[currentSegment];
63
+ if (typeof nextValue !== "object" || nextValue === null) {
64
+ throw new Error(
65
+ `Cannot navigate through path: expected object at segment "${currentSegment}", got ${typeof nextValue}`
66
+ );
67
+ }
68
+ currentObject = nextValue;
69
+ }
70
+ currentObject[lastSegment] = value;
71
+ return object;
72
+ }
73
+
74
+ // src/lib/array-path-to-string-path.ts
75
+ function arrayPathToStringPath(path) {
76
+ return path.reduce((str, item) => {
77
+ if (typeof item === "symbol") {
78
+ return str + (str === "" ? "" : ".") + item.toString();
79
+ }
80
+ if (typeof item === "number" || !Number.isNaN(Number(item))) {
81
+ return str + "[" + item + "]";
82
+ }
83
+ return str + (str === "" ? "" : ".") + item;
84
+ }, "");
85
+ }
86
+
87
+ // src/lib/flat-object-to-nested-object.ts
88
+ function flatObjectToNestedObject(flatStructure) {
89
+ const result = {};
90
+ for (const [path, value] of Object.entries(flatStructure)) {
91
+ setValueAtPath(result, path, value);
92
+ }
93
+ return result;
94
+ }
95
+
96
+ // src/lib/nested-object-to-flat-object.ts
97
+ function nestedObjectToFlatObject(nestedStructure) {
98
+ const result = {};
99
+ function walk(obj, currentPath) {
100
+ if (typeof obj !== "object" || obj === null) {
101
+ result[currentPath] = obj;
102
+ return;
103
+ }
104
+ if (Array.isArray(obj)) {
105
+ obj.forEach((item, index) => {
106
+ const newPath = currentPath ? `${currentPath}[${index}]` : `[${index}]`;
107
+ walk(item, newPath);
108
+ });
109
+ } else {
110
+ for (const [key, value] of Object.entries(obj)) {
111
+ const newPath = currentPath ? `${currentPath}.${key}` : key;
112
+ walk(value, newPath);
113
+ }
114
+ }
115
+ }
116
+ walk(nestedStructure, "");
117
+ return result;
118
+ }
119
+
120
+ // src/lib/get-arrays-diff.ts
121
+ function getArraysDiff(args) {
122
+ const { before, after, key, equals } = args;
123
+ const isEqual = equals;
124
+ const beforeMap = new Map(before.map((b) => [key(b), b]));
125
+ const afterMap = new Map(after.map((a) => [key(a), a]));
126
+ const added = after.filter((a) => !beforeMap.has(key(a)));
127
+ const removed = before.filter((b) => !afterMap.has(key(b)));
128
+ const modified = after.map((a) => {
129
+ const b = beforeMap.get(key(a));
130
+ return b !== void 0 && !isEqual(b, a) ? { before: b, after: a } : void 0;
131
+ }).filter((x) => x !== void 0);
132
+ return { added, removed, modified };
133
+ }
134
+ export {
135
+ arrayPathToStringPath,
136
+ flatObjectToNestedObject,
137
+ getArraysDiff,
138
+ nestedObjectToFlatObject,
139
+ setValueAtPath,
140
+ stringPathToArrayPath
141
+ };
142
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/lib/string-path-to-array-path.ts", "../src/lib/set-value-at-path.ts", "../src/lib/array-path-to-string-path.ts", "../src/lib/flat-object-to-nested-object.ts", "../src/lib/nested-object-to-flat-object.ts", "../src/lib/get-arrays-diff.ts"],
4
+ "sourcesContent": ["// Regular expression to match bracket notation: [key] followed by rest of path.\nconst BRACKET_NOTATION_REGEX = /^\\[(.+?)\\](.*)$/;\n\n// Regular expression to match dot notation: optional dot followed by property\n// name and rest of path.\nconst DOT_NOTATION_REGEX = /^\\.?([^\\.\\[\\]]+)(.*)$/;\n\n// Regular expression to test if a string contains only digits\n// (for array indices).\nconst NUMERIC_KEY_REGEX = /^\\d+$/;\n\n/**\n * Internal recursive parser for path segments.\n * Returns null if the path contains invalid syntax that cannot be parsed.\n *\n * @param currentPath - The remaining path to parse\n * @returns Array of path segments, or null if parsing fails\n */\nfunction parsePath(currentPath: string): Array<string | number> | null {\n // Base case: empty path\n if (currentPath.length === 0) {\n return [];\n }\n\n // Try to match bracket notation first, then dot notation.\n const bracketMatch = currentPath.match(BRACKET_NOTATION_REGEX);\n const dotMatch = currentPath.match(DOT_NOTATION_REGEX);\n\n if (bracketMatch) {\n const [, key = \"\", rest = \"\"] = bracketMatch;\n // Convert numeric keys to numbers, keep string keys as strings.\n const parsedKey = NUMERIC_KEY_REGEX.test(key) ? Number(key) : key;\n\n // Recursively process the rest of the path.\n const restResult = parsePath(rest);\n\n // If rest parsing failed (returned null), propagate failure\n if (restResult === null) {\n return null;\n }\n\n return [parsedKey, ...restResult];\n } else if (dotMatch) {\n const [, key = \"\", rest = \"\"] = dotMatch;\n // Convert numeric keys to numbers, keep string keys as strings.\n const parsedKey = NUMERIC_KEY_REGEX.test(key) ? Number(key) : key;\n\n // Recursively process the rest of the path.\n const restResult = parsePath(rest);\n\n // If rest parsing failed (returned null), propagate failure\n if (restResult === null) {\n return null;\n }\n\n return [parsedKey, ...restResult];\n }\n\n // No pattern matched but we still have content - invalid path\n // Return null to signal failure\n return null;\n}\n\n/**\n * Converts a string path to an array of path segments.\n * Supports both bracket notation (e.g., \"[0]\", \"[key]\") and dot notation (e.g., \".prop\", \"prop\").\n * All numeric strings are converted to numbers regardless of notation type.\n * If the path contains invalid syntax, returns the entire path as a single segment.\n *\n * @param path - The string path to convert (e.g., \"user.profile[0].name\")\n * @returns Array of path segments where numeric strings become numbers\n *\n * @example\n * stringPathToArrayPath(\"user.profile[0].name\") // [\"user\", \"profile\", 0, \"name\"]\n * stringPathToArrayPath(\"users.0[name]\") // [\"users\", 0, \"name\"]\n * stringPathToArrayPath(\"[0].title\") // [0, \"title\"]\n * stringPathToArrayPath(\"\") // []\n * stringPathToArrayPath(\"invalid[[path\") // [\"invalid[[path\"] (invalid syntax)\n */\nexport function stringPathToArrayPath(path: string): Array<string | number> {\n // Handle empty path.\n if (path.length === 0) {\n return [];\n }\n\n // Attempt to parse the path\n const result = parsePath(path);\n\n // If parsing failed, return the entire path as a single segment\n if (result === null) {\n return [path];\n }\n\n return result;\n}\n", "import { stringPathToArrayPath } from \"./string-path-to-array-path.ts\";\n\n/**\n * Sets a value at a specific path within an object, creating nested objects/arrays as needed.\n * The path is parsed using dot notation and bracket notation to navigate through the object structure.\n * Missing intermediate objects are automatically created as objects or arrays based on the next segment type.\n *\n * @param object - The target object to modify (will be mutated)\n * @param path - The path string indicating where to set the value (e.g., \"user.profile[0].name\")\n * @param value - The value to set at the specified path\n * @returns The modified input object (same reference, mutated)\n *\n * @example\n * const obj = {};\n * setValueAtPath(obj, \"user.profile.name\", \"John\");\n * // obj becomes { user: { profile: { name: \"John\" } } }\n *\n * @example\n * const obj = {};\n * setValueAtPath(obj, \"users[0].name\", \"Alice\");\n * // obj becomes { users: [{ name: \"Alice\" }] }\n *\n * @example\n * const obj = {};\n * setValueAtPath(obj, \"config[database][host]\", \"localhost\");\n * // obj becomes { config: { database: { host: \"localhost\" } } }\n */\nexport function setValueAtPath<T extends Record<string | number, unknown>>(\n object: T,\n path: string,\n value: unknown,\n): T {\n const pathSegments = stringPathToArrayPath(path);\n\n // Handle empty path - cannot set value on empty path.\n if (pathSegments.length === 0) {\n throw new Error(\"Cannot set value on empty path\");\n }\n\n // Extract leading segments (all but last) and the final segment.\n const leadingSegments = pathSegments.slice(0, -1);\n const lastSegment = pathSegments[pathSegments.length - 1];\n\n // Type guard for lastSegment.\n if (lastSegment === undefined) {\n throw new Error(\"Invalid path: last segment is undefined\");\n }\n\n // Navigate through the object, creating intermediate objects/arrays as needed.\n let currentObject: Record<string | number, unknown> = object;\n\n for (let i = 0; i < leadingSegments.length; i++) {\n const currentSegment = leadingSegments[i];\n\n // Type guard for currentSegment.\n if (currentSegment === undefined) {\n throw new Error(`Invalid path: segment at index ${i} is undefined`);\n }\n\n // If the current property doesn't exist, create it.\n if (!(currentSegment in currentObject)) {\n // Determine the next segment to decide whether to create an object or array.\n const nextSegment = leadingSegments[i + 1] ?? lastSegment;\n // Create array if next segment is a number, otherwise create object.\n currentObject[currentSegment] = typeof nextSegment === \"number\" ? [] : {};\n }\n\n // Move deeper into the structure.\n const nextValue = currentObject[currentSegment];\n\n // Type guard to ensure we have an object-like structure to navigate into.\n if (typeof nextValue !== \"object\" || nextValue === null) {\n throw new Error(\n `Cannot navigate through path: expected object at segment \"${currentSegment}\", got ${typeof nextValue}`,\n );\n }\n\n currentObject = nextValue as Record<string | number, unknown>;\n }\n\n // Set the final value.\n currentObject[lastSegment] = value;\n\n return object;\n}\n", "/**\n * Convert array path to string format.\n * Examples: [\"user\", \"name\"] -> \"user.name\", [\"items\", 0] -> \"items[0]\"\n *\n * @param path - Array of path segments (strings, numbers, or symbols)\n * @returns String path with dot notation for object properties and bracket notation for array indices\n *\n * @example\n * arrayPathToStringPath([\"user\", \"name\"]) // \"user.name\"\n * arrayPathToStringPath([\"items\", 0]) // \"items[0]\"\n * arrayPathToStringPath([\"addresses\", 0, \"city\"]) // \"addresses[0].city\"\n * arrayPathToStringPath([]) // \"\"\n */\nexport function arrayPathToStringPath(path: Array<PropertyKey>): string {\n return path.reduce((str: string, item) => {\n if (typeof item === \"symbol\") {\n return str + (str === \"\" ? \"\" : \".\") + item.toString();\n }\n if (typeof item === \"number\" || !Number.isNaN(Number(item))) {\n return str + \"[\" + item + \"]\";\n }\n return str + (str === \"\" ? \"\" : \".\") + item;\n }, \"\");\n}\n", "import { setValueAtPath } from \"./set-value-at-path.ts\";\nimport type { FlatObject, NestedObject } from \"./types.ts\";\n\n/**\n * Converts a flat structure object to a nested structure object.\n * Uses path strings like \"user.name\" or \"addresses[0].city\" to create nested structure.\n *\n * @param flatStructure - Flat structure object with string paths as keys\n * @returns Nested structure object with hierarchical structure\n *\n * @example\n * const flat = {\n * \"user.name\": \"John\",\n * \"addresses[0].city\": \"San Francisco\"\n * };\n * const nested = flatObjectToNestedObject(flat);\n * // {\n * // user: { name: \"John\" },\n * // addresses: [{ city: \"San Francisco\" }]\n * // }\n */\nexport function flatObjectToNestedObject<T = unknown, V = unknown>(\n flatStructure: FlatObject<V>,\n): NestedObject<T> {\n const result = {} as Record<string | number, unknown>;\n\n for (const [path, value] of Object.entries(flatStructure)) {\n setValueAtPath(result, path, value);\n }\n\n return result as NestedObject<T>;\n}\n", "import type { FlatObject, NestedObject } from \"./types.ts\";\n\n/**\n * Converts a nested structure object to a flat structure object.\n * Recursively walks the nested structure and creates path strings.\n *\n * @param nestedStructure - Nested structure object with hierarchical structure\n * @returns Flat structure object with string paths as keys\n *\n * @example\n * const nested = {\n * user: { name: \"John\" },\n * addresses: [{ city: \"San Francisco\" }]\n * };\n * const flat = nestedObjectToFlatObject(nested);\n * // {\n * // \"user.name\": \"John\",\n * // \"addresses[0].city\": \"San Francisco\"\n * // }\n */\nexport function nestedObjectToFlatObject<T = unknown, V = unknown>(\n nestedStructure: NestedObject<T>,\n): FlatObject<V> {\n const result: FlatObject<V> = {};\n\n function walk(obj: unknown, currentPath: string) {\n if (typeof obj !== \"object\" || obj === null) {\n // Leaf node - this is a value\n result[currentPath] = obj as V;\n return;\n }\n\n if (Array.isArray(obj)) {\n // Handle arrays\n obj.forEach((item, index) => {\n const newPath = currentPath ? `${currentPath}[${index}]` : `[${index}]`;\n walk(item, newPath);\n });\n } else {\n // Handle objects\n for (const [key, value] of Object.entries(obj)) {\n const newPath = currentPath ? `${currentPath}.${key}` : key;\n walk(value, newPath);\n }\n }\n }\n\n walk(nestedStructure, \"\");\n\n return result;\n}\n", "export interface GetArraysDiffArgs<T extends Record<string, unknown>> {\n before: Array<T>;\n after: Array<T>;\n key: (item: T) => PropertyKey;\n equals: (a: T, b: T) => boolean;\n}\n\nexport function getArraysDiff<T extends Record<string, unknown>>(\n args: GetArraysDiffArgs<T>,\n): {\n added: Array<T>;\n removed: Array<T>;\n modified: Array<{ before: T; after: T }>;\n} {\n const { before, after, key, equals } = args;\n const isEqual = equals;\n\n // Build lookup maps/sets for efficient comparisons by key.\n const beforeMap = new Map(before.map((b) => [key(b), b]));\n const afterMap = new Map(after.map((a) => [key(a), a]));\n\n const added = after.filter((a) => !beforeMap.has(key(a)));\n const removed = before.filter((b) => !afterMap.has(key(b)));\n\n const modified = after\n .map((a): { before: T; after: T } | undefined => {\n const b = beforeMap.get(key(a));\n return b !== undefined && !isEqual(b, a) ? { before: b, after: a } : undefined;\n })\n .filter((x): x is { before: T; after: T } => x !== undefined);\n\n return { added, removed, modified };\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Convert array path to string format.
3
+ * Examples: ["user", "name"] -> "user.name", ["items", 0] -> "items[0]"
4
+ *
5
+ * @param path - Array of path segments (strings, numbers, or symbols)
6
+ * @returns String path with dot notation for object properties and bracket notation for array indices
7
+ *
8
+ * @example
9
+ * arrayPathToStringPath(["user", "name"]) // "user.name"
10
+ * arrayPathToStringPath(["items", 0]) // "items[0]"
11
+ * arrayPathToStringPath(["addresses", 0, "city"]) // "addresses[0].city"
12
+ * arrayPathToStringPath([]) // ""
13
+ */
14
+ export declare function arrayPathToStringPath(path: Array<PropertyKey>): string;
15
+ //# sourceMappingURL=array-path-to-string-path.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"array-path-to-string-path.d.ts","sourceRoot":"","sources":["../../src/lib/array-path-to-string-path.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,KAAK,CAAC,WAAW,CAAC,GAAG,MAAM,CAUtE"}
@@ -0,0 +1,21 @@
1
+ import type { FlatObject, NestedObject } from "./types.ts";
2
+ /**
3
+ * Converts a flat structure object to a nested structure object.
4
+ * Uses path strings like "user.name" or "addresses[0].city" to create nested structure.
5
+ *
6
+ * @param flatStructure - Flat structure object with string paths as keys
7
+ * @returns Nested structure object with hierarchical structure
8
+ *
9
+ * @example
10
+ * const flat = {
11
+ * "user.name": "John",
12
+ * "addresses[0].city": "San Francisco"
13
+ * };
14
+ * const nested = flatObjectToNestedObject(flat);
15
+ * // {
16
+ * // user: { name: "John" },
17
+ * // addresses: [{ city: "San Francisco" }]
18
+ * // }
19
+ */
20
+ export declare function flatObjectToNestedObject<T = unknown, V = unknown>(flatStructure: FlatObject<V>): NestedObject<T>;
21
+ //# sourceMappingURL=flat-object-to-nested-object.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"flat-object-to-nested-object.d.ts","sourceRoot":"","sources":["../../src/lib/flat-object-to-nested-object.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE3D;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,wBAAwB,CAAC,CAAC,GAAG,OAAO,EAAE,CAAC,GAAG,OAAO,EAC/D,aAAa,EAAE,UAAU,CAAC,CAAC,CAAC,GAC3B,YAAY,CAAC,CAAC,CAAC,CAQjB"}
@@ -0,0 +1,15 @@
1
+ export interface GetArraysDiffArgs<T extends Record<string, unknown>> {
2
+ before: Array<T>;
3
+ after: Array<T>;
4
+ key: (item: T) => PropertyKey;
5
+ equals: (a: T, b: T) => boolean;
6
+ }
7
+ export declare function getArraysDiff<T extends Record<string, unknown>>(args: GetArraysDiffArgs<T>): {
8
+ added: Array<T>;
9
+ removed: Array<T>;
10
+ modified: Array<{
11
+ before: T;
12
+ after: T;
13
+ }>;
14
+ };
15
+ //# sourceMappingURL=get-arrays-diff.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"get-arrays-diff.d.ts","sourceRoot":"","sources":["../../src/lib/get-arrays-diff.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,iBAAiB,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAClE,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC;IACjB,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC;IAChB,GAAG,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,WAAW,CAAC;IAC9B,MAAM,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,KAAK,OAAO,CAAC;CACjC;AAED,wBAAgB,aAAa,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7D,IAAI,EAAE,iBAAiB,CAAC,CAAC,CAAC,GACzB;IACD,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC;IAChB,OAAO,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC;IAClB,QAAQ,EAAE,KAAK,CAAC;QAAE,MAAM,EAAE,CAAC,CAAC;QAAC,KAAK,EAAE,CAAC,CAAA;KAAE,CAAC,CAAC;CAC1C,CAmBA"}
@@ -0,0 +1,21 @@
1
+ import type { FlatObject, NestedObject } from "./types.ts";
2
+ /**
3
+ * Converts a nested structure object to a flat structure object.
4
+ * Recursively walks the nested structure and creates path strings.
5
+ *
6
+ * @param nestedStructure - Nested structure object with hierarchical structure
7
+ * @returns Flat structure object with string paths as keys
8
+ *
9
+ * @example
10
+ * const nested = {
11
+ * user: { name: "John" },
12
+ * addresses: [{ city: "San Francisco" }]
13
+ * };
14
+ * const flat = nestedObjectToFlatObject(nested);
15
+ * // {
16
+ * // "user.name": "John",
17
+ * // "addresses[0].city": "San Francisco"
18
+ * // }
19
+ */
20
+ export declare function nestedObjectToFlatObject<T = unknown, V = unknown>(nestedStructure: NestedObject<T>): FlatObject<V>;
21
+ //# sourceMappingURL=nested-object-to-flat-object.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"nested-object-to-flat-object.d.ts","sourceRoot":"","sources":["../../src/lib/nested-object-to-flat-object.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE3D;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,wBAAwB,CAAC,CAAC,GAAG,OAAO,EAAE,CAAC,GAAG,OAAO,EAC/D,eAAe,EAAE,YAAY,CAAC,CAAC,CAAC,GAC/B,UAAU,CAAC,CAAC,CAAC,CA4Bf"}
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Sets a value at a specific path within an object, creating nested objects/arrays as needed.
3
+ * The path is parsed using dot notation and bracket notation to navigate through the object structure.
4
+ * Missing intermediate objects are automatically created as objects or arrays based on the next segment type.
5
+ *
6
+ * @param object - The target object to modify (will be mutated)
7
+ * @param path - The path string indicating where to set the value (e.g., "user.profile[0].name")
8
+ * @param value - The value to set at the specified path
9
+ * @returns The modified input object (same reference, mutated)
10
+ *
11
+ * @example
12
+ * const obj = {};
13
+ * setValueAtPath(obj, "user.profile.name", "John");
14
+ * // obj becomes { user: { profile: { name: "John" } } }
15
+ *
16
+ * @example
17
+ * const obj = {};
18
+ * setValueAtPath(obj, "users[0].name", "Alice");
19
+ * // obj becomes { users: [{ name: "Alice" }] }
20
+ *
21
+ * @example
22
+ * const obj = {};
23
+ * setValueAtPath(obj, "config[database][host]", "localhost");
24
+ * // obj becomes { config: { database: { host: "localhost" } } }
25
+ */
26
+ export declare function setValueAtPath<T extends Record<string | number, unknown>>(object: T, path: string, value: unknown): T;
27
+ //# sourceMappingURL=set-value-at-path.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"set-value-at-path.d.ts","sourceRoot":"","sources":["../../src/lib/set-value-at-path.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,cAAc,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,GAAG,MAAM,EAAE,OAAO,CAAC,EACvE,MAAM,EAAE,CAAC,EACT,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,OAAO,GACb,CAAC,CAqDH"}
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Converts a string path to an array of path segments.
3
+ * Supports both bracket notation (e.g., "[0]", "[key]") and dot notation (e.g., ".prop", "prop").
4
+ * All numeric strings are converted to numbers regardless of notation type.
5
+ * If the path contains invalid syntax, returns the entire path as a single segment.
6
+ *
7
+ * @param path - The string path to convert (e.g., "user.profile[0].name")
8
+ * @returns Array of path segments where numeric strings become numbers
9
+ *
10
+ * @example
11
+ * stringPathToArrayPath("user.profile[0].name") // ["user", "profile", 0, "name"]
12
+ * stringPathToArrayPath("users.0[name]") // ["users", 0, "name"]
13
+ * stringPathToArrayPath("[0].title") // [0, "title"]
14
+ * stringPathToArrayPath("") // []
15
+ * stringPathToArrayPath("invalid[[path") // ["invalid[[path"] (invalid syntax)
16
+ */
17
+ export declare function stringPathToArrayPath(path: string): Array<string | number>;
18
+ //# sourceMappingURL=string-path-to-array-path.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"string-path-to-array-path.d.ts","sourceRoot":"","sources":["../../src/lib/string-path-to-array-path.ts"],"names":[],"mappings":"AA+DA;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,KAAK,CAAC,MAAM,GAAG,MAAM,CAAC,CAe1E"}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Flat structure format - values mapped by string paths
3
+ * @example
4
+ * {
5
+ * "user.name": "John",
6
+ * "addresses[0].city": "San Francisco",
7
+ * "addresses[0].state": "California"
8
+ * }
9
+ */
10
+ export type FlatObject<V = unknown> = Record<string, V>;
11
+ /**
12
+ * Nested structure format - values in a nested object structure
13
+ * @example
14
+ * {
15
+ * user: { name: "John" },
16
+ * addresses: [
17
+ * { city: "San Francisco", state: "California" }
18
+ * ]
19
+ * }
20
+ */
21
+ export type NestedObject<T = unknown> = T extends object ? {
22
+ [K in keyof T]?: T[K] extends object ? NestedObject<T[K]> : unknown;
23
+ } : unknown;
24
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/lib/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,MAAM,MAAM,UAAU,CAAC,CAAC,GAAG,OAAO,IAAI,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;AAExD;;;;;;;;;GASG;AACH,MAAM,MAAM,YAAY,CAAC,CAAC,GAAG,OAAO,IAAI,CAAC,SAAS,MAAM,GACpD;KACG,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,SAAS,MAAM,GAAG,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,OAAO;CACpE,GACD,OAAO,CAAC"}
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@typed-web/form-utils",
3
+ "version": "0.1.1",
4
+ "description": "Shared utilities for form handling: path parsing, error conversion, and array diffing",
5
+ "author": "Łukasz Jagodziński <luke.jagodzinski@gmail.com>",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/typed-web/typed-web.git",
10
+ "directory": "packages/form-utils"
11
+ },
12
+ "homepage": "https://github.com/typed-web/typed-web/tree/main/packages/form-utils#readme",
13
+ "files": [
14
+ "LICENSE",
15
+ "README.md",
16
+ "dist",
17
+ "src",
18
+ "!src/**/*.test.ts"
19
+ ],
20
+ "type": "module",
21
+ "exports": {
22
+ ".": {
23
+ "types": "./dist/index.d.ts",
24
+ "default": "./dist/index.js"
25
+ },
26
+ "./package.json": "./package.json"
27
+ },
28
+ "devDependencies": {
29
+ "@types/node": "^25.0.10",
30
+ "esbuild": "^0.27.2"
31
+ },
32
+ "keywords": [
33
+ "form",
34
+ "utilities",
35
+ "path-parsing",
36
+ "error-conversion",
37
+ "typescript"
38
+ ],
39
+ "scripts": {
40
+ "build": "pnpm run clean && pnpm run build:types && pnpm run build:esm",
41
+ "build:esm": "esbuild src/index.ts --bundle --outfile=dist/index.js --format=esm --platform=neutral --sourcemap",
42
+ "build:types": "tsc --project tsconfig.build.json",
43
+ "clean": "rm -rf dist",
44
+ "test": "node --disable-warning=ExperimentalWarning --test './src/**/*.test.ts'",
45
+ "typecheck": "tsc --noEmit"
46
+ }
47
+ }
package/src/index.ts ADDED
@@ -0,0 +1,15 @@
1
+ // Types
2
+ export type { FlatObject, NestedObject } from "./lib/types.ts";
3
+
4
+ // Path utilities
5
+ export { setValueAtPath } from "./lib/set-value-at-path.ts";
6
+ export { stringPathToArrayPath } from "./lib/string-path-to-array-path.ts";
7
+ export { arrayPathToStringPath } from "./lib/array-path-to-string-path.ts";
8
+
9
+ // Structure conversion
10
+ export { flatObjectToNestedObject } from "./lib/flat-object-to-nested-object.ts";
11
+ export { nestedObjectToFlatObject } from "./lib/nested-object-to-flat-object.ts";
12
+
13
+ // Array utilities
14
+ export { getArraysDiff } from "./lib/get-arrays-diff.ts";
15
+ export type { GetArraysDiffArgs } from "./lib/get-arrays-diff.ts";
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Convert array path to string format.
3
+ * Examples: ["user", "name"] -> "user.name", ["items", 0] -> "items[0]"
4
+ *
5
+ * @param path - Array of path segments (strings, numbers, or symbols)
6
+ * @returns String path with dot notation for object properties and bracket notation for array indices
7
+ *
8
+ * @example
9
+ * arrayPathToStringPath(["user", "name"]) // "user.name"
10
+ * arrayPathToStringPath(["items", 0]) // "items[0]"
11
+ * arrayPathToStringPath(["addresses", 0, "city"]) // "addresses[0].city"
12
+ * arrayPathToStringPath([]) // ""
13
+ */
14
+ export function arrayPathToStringPath(path: Array<PropertyKey>): string {
15
+ return path.reduce((str: string, item) => {
16
+ if (typeof item === "symbol") {
17
+ return str + (str === "" ? "" : ".") + item.toString();
18
+ }
19
+ if (typeof item === "number" || !Number.isNaN(Number(item))) {
20
+ return str + "[" + item + "]";
21
+ }
22
+ return str + (str === "" ? "" : ".") + item;
23
+ }, "");
24
+ }
@@ -0,0 +1,32 @@
1
+ import { setValueAtPath } from "./set-value-at-path.ts";
2
+ import type { FlatObject, NestedObject } from "./types.ts";
3
+
4
+ /**
5
+ * Converts a flat structure object to a nested structure object.
6
+ * Uses path strings like "user.name" or "addresses[0].city" to create nested structure.
7
+ *
8
+ * @param flatStructure - Flat structure object with string paths as keys
9
+ * @returns Nested structure object with hierarchical structure
10
+ *
11
+ * @example
12
+ * const flat = {
13
+ * "user.name": "John",
14
+ * "addresses[0].city": "San Francisco"
15
+ * };
16
+ * const nested = flatObjectToNestedObject(flat);
17
+ * // {
18
+ * // user: { name: "John" },
19
+ * // addresses: [{ city: "San Francisco" }]
20
+ * // }
21
+ */
22
+ export function flatObjectToNestedObject<T = unknown, V = unknown>(
23
+ flatStructure: FlatObject<V>,
24
+ ): NestedObject<T> {
25
+ const result = {} as Record<string | number, unknown>;
26
+
27
+ for (const [path, value] of Object.entries(flatStructure)) {
28
+ setValueAtPath(result, path, value);
29
+ }
30
+
31
+ return result as NestedObject<T>;
32
+ }
@@ -0,0 +1,33 @@
1
+ export interface GetArraysDiffArgs<T extends Record<string, unknown>> {
2
+ before: Array<T>;
3
+ after: Array<T>;
4
+ key: (item: T) => PropertyKey;
5
+ equals: (a: T, b: T) => boolean;
6
+ }
7
+
8
+ export function getArraysDiff<T extends Record<string, unknown>>(
9
+ args: GetArraysDiffArgs<T>,
10
+ ): {
11
+ added: Array<T>;
12
+ removed: Array<T>;
13
+ modified: Array<{ before: T; after: T }>;
14
+ } {
15
+ const { before, after, key, equals } = args;
16
+ const isEqual = equals;
17
+
18
+ // Build lookup maps/sets for efficient comparisons by key.
19
+ const beforeMap = new Map(before.map((b) => [key(b), b]));
20
+ const afterMap = new Map(after.map((a) => [key(a), a]));
21
+
22
+ const added = after.filter((a) => !beforeMap.has(key(a)));
23
+ const removed = before.filter((b) => !afterMap.has(key(b)));
24
+
25
+ const modified = after
26
+ .map((a): { before: T; after: T } | undefined => {
27
+ const b = beforeMap.get(key(a));
28
+ return b !== undefined && !isEqual(b, a) ? { before: b, after: a } : undefined;
29
+ })
30
+ .filter((x): x is { before: T; after: T } => x !== undefined);
31
+
32
+ return { added, removed, modified };
33
+ }
@@ -0,0 +1,51 @@
1
+ import type { FlatObject, NestedObject } from "./types.ts";
2
+
3
+ /**
4
+ * Converts a nested structure object to a flat structure object.
5
+ * Recursively walks the nested structure and creates path strings.
6
+ *
7
+ * @param nestedStructure - Nested structure object with hierarchical structure
8
+ * @returns Flat structure object with string paths as keys
9
+ *
10
+ * @example
11
+ * const nested = {
12
+ * user: { name: "John" },
13
+ * addresses: [{ city: "San Francisco" }]
14
+ * };
15
+ * const flat = nestedObjectToFlatObject(nested);
16
+ * // {
17
+ * // "user.name": "John",
18
+ * // "addresses[0].city": "San Francisco"
19
+ * // }
20
+ */
21
+ export function nestedObjectToFlatObject<T = unknown, V = unknown>(
22
+ nestedStructure: NestedObject<T>,
23
+ ): FlatObject<V> {
24
+ const result: FlatObject<V> = {};
25
+
26
+ function walk(obj: unknown, currentPath: string) {
27
+ if (typeof obj !== "object" || obj === null) {
28
+ // Leaf node - this is a value
29
+ result[currentPath] = obj as V;
30
+ return;
31
+ }
32
+
33
+ if (Array.isArray(obj)) {
34
+ // Handle arrays
35
+ obj.forEach((item, index) => {
36
+ const newPath = currentPath ? `${currentPath}[${index}]` : `[${index}]`;
37
+ walk(item, newPath);
38
+ });
39
+ } else {
40
+ // Handle objects
41
+ for (const [key, value] of Object.entries(obj)) {
42
+ const newPath = currentPath ? `${currentPath}.${key}` : key;
43
+ walk(value, newPath);
44
+ }
45
+ }
46
+ }
47
+
48
+ walk(nestedStructure, "");
49
+
50
+ return result;
51
+ }
@@ -0,0 +1,85 @@
1
+ import { stringPathToArrayPath } from "./string-path-to-array-path.ts";
2
+
3
+ /**
4
+ * Sets a value at a specific path within an object, creating nested objects/arrays as needed.
5
+ * The path is parsed using dot notation and bracket notation to navigate through the object structure.
6
+ * Missing intermediate objects are automatically created as objects or arrays based on the next segment type.
7
+ *
8
+ * @param object - The target object to modify (will be mutated)
9
+ * @param path - The path string indicating where to set the value (e.g., "user.profile[0].name")
10
+ * @param value - The value to set at the specified path
11
+ * @returns The modified input object (same reference, mutated)
12
+ *
13
+ * @example
14
+ * const obj = {};
15
+ * setValueAtPath(obj, "user.profile.name", "John");
16
+ * // obj becomes { user: { profile: { name: "John" } } }
17
+ *
18
+ * @example
19
+ * const obj = {};
20
+ * setValueAtPath(obj, "users[0].name", "Alice");
21
+ * // obj becomes { users: [{ name: "Alice" }] }
22
+ *
23
+ * @example
24
+ * const obj = {};
25
+ * setValueAtPath(obj, "config[database][host]", "localhost");
26
+ * // obj becomes { config: { database: { host: "localhost" } } }
27
+ */
28
+ export function setValueAtPath<T extends Record<string | number, unknown>>(
29
+ object: T,
30
+ path: string,
31
+ value: unknown,
32
+ ): T {
33
+ const pathSegments = stringPathToArrayPath(path);
34
+
35
+ // Handle empty path - cannot set value on empty path.
36
+ if (pathSegments.length === 0) {
37
+ throw new Error("Cannot set value on empty path");
38
+ }
39
+
40
+ // Extract leading segments (all but last) and the final segment.
41
+ const leadingSegments = pathSegments.slice(0, -1);
42
+ const lastSegment = pathSegments[pathSegments.length - 1];
43
+
44
+ // Type guard for lastSegment.
45
+ if (lastSegment === undefined) {
46
+ throw new Error("Invalid path: last segment is undefined");
47
+ }
48
+
49
+ // Navigate through the object, creating intermediate objects/arrays as needed.
50
+ let currentObject: Record<string | number, unknown> = object;
51
+
52
+ for (let i = 0; i < leadingSegments.length; i++) {
53
+ const currentSegment = leadingSegments[i];
54
+
55
+ // Type guard for currentSegment.
56
+ if (currentSegment === undefined) {
57
+ throw new Error(`Invalid path: segment at index ${i} is undefined`);
58
+ }
59
+
60
+ // If the current property doesn't exist, create it.
61
+ if (!(currentSegment in currentObject)) {
62
+ // Determine the next segment to decide whether to create an object or array.
63
+ const nextSegment = leadingSegments[i + 1] ?? lastSegment;
64
+ // Create array if next segment is a number, otherwise create object.
65
+ currentObject[currentSegment] = typeof nextSegment === "number" ? [] : {};
66
+ }
67
+
68
+ // Move deeper into the structure.
69
+ const nextValue = currentObject[currentSegment];
70
+
71
+ // Type guard to ensure we have an object-like structure to navigate into.
72
+ if (typeof nextValue !== "object" || nextValue === null) {
73
+ throw new Error(
74
+ `Cannot navigate through path: expected object at segment "${currentSegment}", got ${typeof nextValue}`,
75
+ );
76
+ }
77
+
78
+ currentObject = nextValue as Record<string | number, unknown>;
79
+ }
80
+
81
+ // Set the final value.
82
+ currentObject[lastSegment] = value;
83
+
84
+ return object;
85
+ }
@@ -0,0 +1,95 @@
1
+ // Regular expression to match bracket notation: [key] followed by rest of path.
2
+ const BRACKET_NOTATION_REGEX = /^\[(.+?)\](.*)$/;
3
+
4
+ // Regular expression to match dot notation: optional dot followed by property
5
+ // name and rest of path.
6
+ const DOT_NOTATION_REGEX = /^\.?([^\.\[\]]+)(.*)$/;
7
+
8
+ // Regular expression to test if a string contains only digits
9
+ // (for array indices).
10
+ const NUMERIC_KEY_REGEX = /^\d+$/;
11
+
12
+ /**
13
+ * Internal recursive parser for path segments.
14
+ * Returns null if the path contains invalid syntax that cannot be parsed.
15
+ *
16
+ * @param currentPath - The remaining path to parse
17
+ * @returns Array of path segments, or null if parsing fails
18
+ */
19
+ function parsePath(currentPath: string): Array<string | number> | null {
20
+ // Base case: empty path
21
+ if (currentPath.length === 0) {
22
+ return [];
23
+ }
24
+
25
+ // Try to match bracket notation first, then dot notation.
26
+ const bracketMatch = currentPath.match(BRACKET_NOTATION_REGEX);
27
+ const dotMatch = currentPath.match(DOT_NOTATION_REGEX);
28
+
29
+ if (bracketMatch) {
30
+ const [, key = "", rest = ""] = bracketMatch;
31
+ // Convert numeric keys to numbers, keep string keys as strings.
32
+ const parsedKey = NUMERIC_KEY_REGEX.test(key) ? Number(key) : key;
33
+
34
+ // Recursively process the rest of the path.
35
+ const restResult = parsePath(rest);
36
+
37
+ // If rest parsing failed (returned null), propagate failure
38
+ if (restResult === null) {
39
+ return null;
40
+ }
41
+
42
+ return [parsedKey, ...restResult];
43
+ } else if (dotMatch) {
44
+ const [, key = "", rest = ""] = dotMatch;
45
+ // Convert numeric keys to numbers, keep string keys as strings.
46
+ const parsedKey = NUMERIC_KEY_REGEX.test(key) ? Number(key) : key;
47
+
48
+ // Recursively process the rest of the path.
49
+ const restResult = parsePath(rest);
50
+
51
+ // If rest parsing failed (returned null), propagate failure
52
+ if (restResult === null) {
53
+ return null;
54
+ }
55
+
56
+ return [parsedKey, ...restResult];
57
+ }
58
+
59
+ // No pattern matched but we still have content - invalid path
60
+ // Return null to signal failure
61
+ return null;
62
+ }
63
+
64
+ /**
65
+ * Converts a string path to an array of path segments.
66
+ * Supports both bracket notation (e.g., "[0]", "[key]") and dot notation (e.g., ".prop", "prop").
67
+ * All numeric strings are converted to numbers regardless of notation type.
68
+ * If the path contains invalid syntax, returns the entire path as a single segment.
69
+ *
70
+ * @param path - The string path to convert (e.g., "user.profile[0].name")
71
+ * @returns Array of path segments where numeric strings become numbers
72
+ *
73
+ * @example
74
+ * stringPathToArrayPath("user.profile[0].name") // ["user", "profile", 0, "name"]
75
+ * stringPathToArrayPath("users.0[name]") // ["users", 0, "name"]
76
+ * stringPathToArrayPath("[0].title") // [0, "title"]
77
+ * stringPathToArrayPath("") // []
78
+ * stringPathToArrayPath("invalid[[path") // ["invalid[[path"] (invalid syntax)
79
+ */
80
+ export function stringPathToArrayPath(path: string): Array<string | number> {
81
+ // Handle empty path.
82
+ if (path.length === 0) {
83
+ return [];
84
+ }
85
+
86
+ // Attempt to parse the path
87
+ const result = parsePath(path);
88
+
89
+ // If parsing failed, return the entire path as a single segment
90
+ if (result === null) {
91
+ return [path];
92
+ }
93
+
94
+ return result;
95
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Flat structure format - values mapped by string paths
3
+ * @example
4
+ * {
5
+ * "user.name": "John",
6
+ * "addresses[0].city": "San Francisco",
7
+ * "addresses[0].state": "California"
8
+ * }
9
+ */
10
+ export type FlatObject<V = unknown> = Record<string, V>;
11
+
12
+ /**
13
+ * Nested structure format - values in a nested object structure
14
+ * @example
15
+ * {
16
+ * user: { name: "John" },
17
+ * addresses: [
18
+ * { city: "San Francisco", state: "California" }
19
+ * ]
20
+ * }
21
+ */
22
+ export type NestedObject<T = unknown> = T extends object
23
+ ? {
24
+ [K in keyof T]?: T[K] extends object ? NestedObject<T[K]> : unknown;
25
+ }
26
+ : unknown;