@helpers4/object 2.0.3 → 2.0.4

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/lib/index.d.ts CHANGED
@@ -35,15 +35,21 @@ declare function deepClone<T>(obj: T): T;
35
35
  * SPDX-License-Identifier: LGPL-3.0-or-later
36
36
  */
37
37
  /**
38
- * Merges two or more objects deeply
39
- * @param target - The target object
40
- * @param sources - The source objects to merge
41
- * @returns The merged object
38
+ * Merges two or more objects deeply.
39
+ *
40
+ * Recursively merges own enumerable properties — both string and symbol keys.
41
+ * Plain objects are merged recursively; all other values (arrays, class instances,
42
+ * primitives, etc.) are replaced by the source value.
43
+ * `undefined` source values do not overwrite existing target values.
44
+ *
45
+ * @param target - The target object (mutated in place)
46
+ * @param sources - One or more source objects to merge into the target
47
+ * @returns The mutated target
42
48
  * @since 1.9.0
43
49
  */
44
- declare function deepMerge<T extends Record<string, unknown>>(target: T, ...sources: Record<string, unknown>[]): T;
45
- declare function deepMerge(target: undefined, ...sources: Record<string, unknown>[]): undefined;
46
- declare function deepMerge(target: null, ...sources: Record<string, unknown>[]): null;
50
+ declare function deepMerge<T extends Record<PropertyKey, unknown>>(target: T, ...sources: Record<PropertyKey, unknown>[]): T;
51
+ declare function deepMerge(target: undefined, ...sources: Record<PropertyKey, unknown>[]): undefined;
52
+ declare function deepMerge(target: null, ...sources: Record<PropertyKey, unknown>[]): null;
47
53
 
48
54
  /**
49
55
  * This file is part of helpers4.
@@ -163,14 +169,65 @@ declare function equalsShallow(objA: unknown, objB: unknown): boolean;
163
169
  * SPDX-License-Identifier: LGPL-3.0-or-later
164
170
  */
165
171
  /**
166
- * Gets a value from an object using a dot-notated path
167
- * @param obj - The object to get value from
168
- * @param path - The dot-notated path (e.g., 'a.b.c')
169
- * @param defaultValue - Default value if path doesn't exist
170
- * @returns The value at the path or default value
172
+ * Resolves the value type at a given path within T.
173
+ * Returns `unknown` when the path doesn't match the shape of T.
174
+ *
175
+ * @internal
176
+ * @example
177
+ * DeepGet<{ a: { b: number } }, ['a', 'b']> // => number
178
+ * DeepGet<{ a: { b: number } }, ['a', 'x']> // => unknown
179
+ * DeepGet<{ a: number }, []> // => { a: number }
180
+ */
181
+ type DeepGet<T, Path extends readonly PropertyKey[]> = Path extends readonly [] ? T : Path extends readonly [infer K, ...infer Rest extends readonly PropertyKey[]] ? K extends keyof T ? DeepGet<T[K], Rest> : unknown : unknown;
182
+ /**
183
+ * Produces the type of T after setting the value at Path to V.
184
+ * Resolves to `never` when a key in Path is not present in the corresponding level of T,
185
+ * surfacing invalid paths at the call site rather than silently returning T unchanged.
186
+ *
187
+ * @internal
188
+ * @example
189
+ * DeepSet<{ a: { b: number } }, ['a', 'b'], string>
190
+ * // => { a: { b: string } }
191
+ */
192
+ type DeepSet<T, Path extends readonly PropertyKey[], V> = Path extends readonly [] ? T : Path extends readonly [infer K, ...infer Rest extends readonly PropertyKey[]] ? K extends keyof T ? {
193
+ [P in keyof T]: P extends K ? DeepSet<T[K], Rest, V> : T[P];
194
+ } : never : never;
195
+
196
+ /**
197
+ * This file is part of helpers4.
198
+ * Copyright (C) 2025 baxyz
199
+ * SPDX-License-Identifier: LGPL-3.0-or-later
200
+ */
201
+
202
+ /**
203
+ * Gets a value from an object using a dot/bracket-notated path or explicit key array.
204
+ *
205
+ * **Two path forms are supported:**
206
+ *
207
+ * 1. **String path** — dot notation (`'a.b.c'`) and bracket notation (`'layers[1].name'`)
208
+ * are both accepted and mixed freely. Segments are traversed as string keys; `[n]`
209
+ * indices become numeric keys.
210
+ *
211
+ * 2. **Key array** (`PropertyKey[]`) — explicit array of `string | number | symbol` keys,
212
+ * no parsing performed. Enables symbol-keyed traversal and compile-time type inference:
213
+ * `get(obj, ['a', 'b'] as const)` infers the return type from the path.
214
+ *
215
+ * @param obj - The object to read from
216
+ * @param path - Dot/bracket-notation string or explicit `PropertyKey[]`
217
+ * @param defaultValue - Returned when the path is absent or resolves to `undefined`
218
+ * @returns The value at the path, or `defaultValue`
219
+ * @example
220
+ * get({ a: { b: { c: 42 } } }, 'a.b.c') // => 42
221
+ * get({ a: [1, 2, 3] }, 'a[1]') // => 2
222
+ * get({ a: { b: 0 } }, 'a.b', 99) // => 0 (falsy values are returned as-is)
223
+ * get({ a: 1 }, 'b.c', 'default') // => 'default'
224
+ *
225
+ * const id = Symbol('id')
226
+ * get({ [id]: 'alice' }, [id]) // => 'alice' (symbol key)
171
227
  * @since 1.9.0
172
228
  */
173
229
  declare function get<T = unknown>(obj: unknown, path: string, defaultValue?: T): T | undefined;
230
+ declare function get<T extends object, const Path extends readonly PropertyKey[]>(obj: T, path: Path, defaultValue?: DeepGet<T, Path>): DeepGet<T, Path> | undefined;
174
231
 
175
232
  /**
176
233
  * This file is part of helpers4.
@@ -385,15 +442,51 @@ declare function safeJsonParse<T>(input: string, fallback: T): T;
385
442
  * Copyright (C) 2025 baxyz
386
443
  * SPDX-License-Identifier: LGPL-3.0-or-later
387
444
  */
445
+
388
446
  /**
389
- * Sets a value in an object using a dot-notated path
390
- * @param obj - The object to set value in
391
- * @param path - The dot-notated path (e.g., 'a.b.c')
392
- * @param value - The value to set
393
- * @returns The modified object
447
+ * Sets a value in an object at the given path, creating intermediate objects as needed.
448
+ *
449
+ * **Three path forms are supported:**
450
+ *
451
+ * 1. **Dot notation** (`string`) — segments split on `.` are kept as-is string keys.
452
+ * `"layers.1.name"` → keys `["layers", "1", "name"]` (all strings, including `"1"`).
453
+ *
454
+ * 2. **Bracket notation** (`string`) — `[n]` segments are parsed as numeric keys.
455
+ * `"layers[1].name"` → keys `["layers", 1, "name"]` (index `1` becomes a number).
456
+ * Dot and bracket can be mixed freely: `"a[0].b[2].c"`.
457
+ *
458
+ * 3. **Key array** (`PropertyKey[]`) — explicit array of `string | number | symbol` keys,
459
+ * no parsing performed. Enables full key-type control, including symbols:
460
+ * `["layers", 1, Symbol('id')]`.
461
+ *
462
+ * Intermediate nodes that are absent, `null`, or not an object are replaced with `{}`.
463
+ * Any path containing a string segment equal to `__proto__`, `constructor`, or `prototype`
464
+ * is rejected and the original object is returned unchanged (prototype-pollution guard).
465
+ *
466
+ * @param obj - The object to mutate
467
+ * @param path - Dot/bracket-notation string or explicit `PropertyKey[]`
468
+ * @param value - Value to assign at the path
469
+ * @returns The mutated object (same reference)
470
+ * @example
471
+ * // 1. Dot notation — "1" stays a string key
472
+ * set({}, 'a.b.c', 42)
473
+ * // => { a: { b: { c: 42 } } }
474
+ *
475
+ * set({}, 'layers.1.name', 'bg')
476
+ * // => { layers: { '1': { name: 'bg' } } }
477
+ *
478
+ * // 2. Bracket notation — [1] becomes a number key
479
+ * set({ layers: [{}, { name: 'old' }] }, 'layers[1].name', 'bg')
480
+ * // => { layers: [{}, { name: 'bg' }] }
481
+ *
482
+ * // 3. Key array — supports symbols
483
+ * const id = Symbol('id')
484
+ * set({}, ['user', id], 'alice')
485
+ * // => { user: { [id]: 'alice' } }
394
486
  * @since 1.9.0
395
487
  */
396
488
  declare function set(obj: Record<string, unknown>, path: string, value: unknown): Record<string, unknown>;
489
+ declare function set<T extends object, const Path extends readonly PropertyKey[], V extends DeepGet<T, Path>>(obj: T, path: Path, value: V): DeepSet<T, Path, V>;
397
490
 
398
491
  export { compact, deepClone, deepMerge, diff, equalsDeep, equalsShallow, get, groupBy, invert, isEmpty, isNonEmpty, map, omit, pick, removeUndefinedNull, safeJsonParse, set };
399
492
  export type { DiffOptions, DiffResult };
package/lib/index.js CHANGED
@@ -6,17 +6,28 @@ function compact(obj) {
6
6
  return result;
7
7
  }
8
8
  //#endregion
9
- //#region helpers/object/deepClone.ts
9
+ //#region helpers/object/_unsafeKeys.ts
10
10
  /**
11
11
  * This file is part of helpers4.
12
12
  * Copyright (C) 2025 baxyz
13
13
  * SPDX-License-Identifier: LGPL-3.0-or-later
14
14
  */
15
- var UNSAFE_KEYS$2 = new Set([
15
+ /**
16
+ * Keys that must never be set on a plain object (prototype-pollution guard).
17
+ * @internal
18
+ */
19
+ var UNSAFE_KEYS = new Set([
16
20
  "__proto__",
17
21
  "constructor",
18
22
  "prototype"
19
23
  ]);
24
+ //#endregion
25
+ //#region helpers/object/deepClone.ts
26
+ /**
27
+ * This file is part of helpers4.
28
+ * Copyright (C) 2025 baxyz
29
+ * SPDX-License-Identifier: LGPL-3.0-or-later
30
+ */
20
31
  /**
21
32
  * Creates a deep copy of an object or array
22
33
  * @param obj - The object to clone
@@ -28,7 +39,7 @@ function deepClone(obj) {
28
39
  if (obj instanceof Date) return new Date(obj.getTime());
29
40
  if (Array.isArray(obj)) return obj.map((item) => deepClone(item));
30
41
  const cloned = {};
31
- for (const key in obj) if (Object.prototype.hasOwnProperty.call(obj, key) && !UNSAFE_KEYS$2.has(key)) cloned[key] = deepClone(obj[key]);
42
+ for (const key in obj) if (Object.prototype.hasOwnProperty.call(obj, key) && !UNSAFE_KEYS.has(key)) cloned[key] = deepClone(obj[key]);
32
43
  return cloned;
33
44
  }
34
45
  //#endregion
@@ -66,22 +77,26 @@ function isPlainObject(value) {
66
77
  * Copyright (C) 2025 baxyz
67
78
  * SPDX-License-Identifier: LGPL-3.0-or-later
68
79
  */
69
- var UNSAFE_KEYS$1 = new Set([
70
- "__proto__",
71
- "constructor",
72
- "prototype"
73
- ]);
80
+ function mergeKey(target, source, key) {
81
+ const targetValue = target[key];
82
+ const sourceValue = source[key];
83
+ if (isPlainObject(targetValue) && isPlainObject(sourceValue)) target[key] = deepMerge(targetValue, sourceValue);
84
+ else if (sourceValue !== void 0) target[key] = sourceValue;
85
+ }
74
86
  function deepMerge(target, ...sources) {
75
87
  if (target === void 0 || target === null) return target;
76
88
  if (!sources.length) return target;
77
89
  const source = sources.shift();
78
90
  if (!source) return deepMerge(target, ...sources);
91
+ const t = target;
92
+ const s = source;
79
93
  for (const key in source) {
80
- if (!Object.hasOwn(source, key) || UNSAFE_KEYS$1.has(key)) continue;
81
- const targetValue = target[key];
82
- const sourceValue = source[key];
83
- if (isPlainObject(targetValue) && isPlainObject(sourceValue)) target[key] = deepMerge(targetValue, sourceValue);
84
- else if (sourceValue !== void 0) target[key] = sourceValue;
94
+ if (!Object.hasOwn(source, key) || UNSAFE_KEYS.has(key)) continue;
95
+ mergeKey(t, s, key);
96
+ }
97
+ for (const sym of Object.getOwnPropertySymbols(source)) {
98
+ if (!Object.prototype.propertyIsEnumerable.call(source, sym)) continue;
99
+ mergeKey(t, s, sym);
85
100
  }
86
101
  return deepMerge(target, ...sources);
87
102
  }
@@ -443,22 +458,70 @@ function equalsShallow(objA, objB) {
443
458
  return true;
444
459
  }
445
460
  //#endregion
446
- //#region helpers/object/get.ts
461
+ //#region helpers/object/_parsePath.ts
447
462
  /**
448
463
  * This file is part of helpers4.
449
464
  * Copyright (C) 2025 baxyz
450
465
  * SPDX-License-Identifier: LGPL-3.0-or-later
451
466
  */
452
467
  /**
453
- * Gets a value from an object using a dot-notated path
454
- * @param obj - The object to get value from
455
- * @param path - The dot-notated path (e.g., 'a.b.c')
456
- * @param defaultValue - Default value if path doesn't exist
457
- * @returns The value at the path or default value
458
- * @since 1.9.0
468
+ * Parses a string path into an array of string/number segments.
469
+ *
470
+ * - Dot separators (`.`) split segments; each segment becomes a string key.
471
+ * - Bracket indices (`[n]`) become number keys.
472
+ * - A leading `.` is treated as "current level" and stripped before parsing,
473
+ * so `.[0]` ≡ `[0]` and `.a.b` ≡ `a.b`.
474
+ * - Empty string (or bare `.`) returns `['']` (addresses the `''` key on the root object).
475
+ * - Consecutive dots (`a..b`) produce an empty-string segment: `['a', '', 'b']`.
476
+ * - Non-bracket text trailing a `]` within the same dot-segment throws a RangeError;
477
+ * use a dot separator instead (`'a[0].b'` not `'a[0]b'`).
478
+ *
479
+ * @internal
480
+ * @example
481
+ * parsePath('a.b.c') // => ['a', 'b', 'c'] (all strings)
482
+ * parsePath('layers.1.x') // => ['layers', '1', 'x'] (all strings)
483
+ * parsePath('layers[1].x') // => ['layers', 1, 'x'] (bracket → number)
484
+ * parsePath('.[0]') // => [0] (leading dot stripped)
485
+ * parsePath('a..b') // => ['a', '', 'b'] (empty segment preserved)
486
+ * parsePath('.') // => [''] (leading dot → empty-string key)
487
+ */
488
+ var BRACKET = /\[(\d+)\]/g;
489
+ var parsePathCache = /* @__PURE__ */ new Map();
490
+ function parsePath(path) {
491
+ const cached = parsePathCache.get(path);
492
+ if (cached !== void 0) return cached;
493
+ const result = Object.freeze(parsePathUncached(path));
494
+ parsePathCache.set(path, result);
495
+ return result;
496
+ }
497
+ function parsePathUncached(path) {
498
+ if (path === "") return [""];
499
+ if (path[0] === ".") path = path.slice(1);
500
+ if (path === "") return [""];
501
+ const result = [];
502
+ for (const dotSegment of path.split(".")) {
503
+ BRACKET.lastIndex = 0;
504
+ let last = 0;
505
+ let match;
506
+ while ((match = BRACKET.exec(dotSegment)) !== null) {
507
+ if (match.index > last) result.push(dotSegment.slice(last, match.index));
508
+ result.push(parseInt(match[1], 10));
509
+ last = BRACKET.lastIndex;
510
+ }
511
+ if (last === 0) result.push(dotSegment);
512
+ else if (last < dotSegment.length) throw new RangeError(`parsePath: non-bracket text '${dotSegment.slice(last)}' after ']' in segment '${dotSegment}' — use 'a[0].b' instead of 'a[0]b'`);
513
+ }
514
+ return result;
515
+ }
516
+ //#endregion
517
+ //#region helpers/object/get.ts
518
+ /**
519
+ * This file is part of helpers4.
520
+ * Copyright (C) 2025 baxyz
521
+ * SPDX-License-Identifier: LGPL-3.0-or-later
459
522
  */
460
523
  function get(obj, path, defaultValue) {
461
- const keys = path.split(".");
524
+ const keys = typeof path === "string" ? parsePath(path) : path;
462
525
  let result = obj;
463
526
  for (const key of keys) {
464
527
  if (result == null || typeof result !== "object") return defaultValue;
@@ -688,26 +751,14 @@ function safeJsonParse(input, fallback) {
688
751
  * Copyright (C) 2025 baxyz
689
752
  * SPDX-License-Identifier: LGPL-3.0-or-later
690
753
  */
691
- var UNSAFE_KEYS = new Set([
692
- "__proto__",
693
- "constructor",
694
- "prototype"
695
- ]);
696
- /**
697
- * Sets a value in an object using a dot-notated path
698
- * @param obj - The object to set value in
699
- * @param path - The dot-notated path (e.g., 'a.b.c')
700
- * @param value - The value to set
701
- * @returns The modified object
702
- * @since 1.9.0
703
- */
704
754
  function set(obj, path, value) {
705
- const keys = path.split(".");
706
- if (keys.some((k) => UNSAFE_KEYS.has(k))) return obj;
755
+ const keys = typeof path === "string" ? parsePath(path) : path;
756
+ if (keys.length === 0) return obj;
757
+ if (keys.some((k) => typeof k === "string" && UNSAFE_KEYS.has(k))) return obj;
707
758
  let current = obj;
708
759
  for (let i = 0; i < keys.length - 1; i++) {
709
760
  const key = keys[i];
710
- if (!(key in current) || typeof current[key] !== "object" || current[key] === null) current[key] = {};
761
+ if (!Object.hasOwn(current, key) || typeof current[key] !== "object" || current[key] === null) current[key] = {};
711
762
  current = current[key];
712
763
  }
713
764
  current[keys[keys.length - 1]] = value;
package/lib/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../../helpers/object/compact.ts","../../../helpers/object/deepClone.ts","../../../helpers/type/isPlainObject.ts","../../../helpers/object/deepMerge.ts","../../../helpers/array/equalsDeep.ts","../../../helpers/date/timestamp.ts","../../../helpers/date/ensureDate.ts","../../../helpers/date/compare.ts","../../../helpers/type/isSpecialObject.ts","../../../helpers/object/diff.ts","../../../helpers/object/equalsDeep.ts","../../../helpers/object/equalsShallow.ts","../../../helpers/object/get.ts","../../../helpers/object/groupBy.ts","../../../helpers/object/invert.ts","../../../helpers/object/isEmpty.ts","../../../helpers/object/isNonEmpty.ts","../../../helpers/object/map.ts","../../../helpers/object/omit.ts","../../../helpers/object/pick.ts","../../../helpers/type/isNullish.ts","../../../helpers/object/removeUndefinedNull.ts","../../../helpers/object/safeJsonParse.ts","../../../helpers/object/set.ts"],"sourcesContent":["/**\n * This file is part of helpers4.\n * Copyright (C) 2025 baxyz\n * SPDX-License-Identifier: LGPL-3.0-or-later\n */\n\n/**\n * Removes all entries with falsy values (`false`, `null`, `undefined`, `0`, `\"\"`, `NaN`) from an object.\n * @param obj - The source object\n * @returns A new object containing only entries with truthy values\n * @example\n * compact({ a: 1, b: null, c: '', d: 0, e: 'hello' })\n * // => { a: 1, e: 'hello' }\n * @since 2.0.0\n */\nexport function compact<T extends Record<string, unknown>>(obj: T): Partial<T>;\nexport function compact(obj: undefined): undefined;\nexport function compact(obj: null): null;\nexport function compact<T extends Record<string, unknown>>(obj: T | undefined | null): Partial<T> | undefined | null {\n if (obj === undefined || obj === null) return obj;\n const result: Partial<T> = {};\n for (const key of Object.keys(obj) as (keyof T)[]) {\n if (obj[key]) {\n result[key] = obj[key];\n }\n }\n return result;\n}\n","/**\n * This file is part of helpers4.\n * Copyright (C) 2025 baxyz\n * SPDX-License-Identifier: LGPL-3.0-or-later\n */\n\nconst UNSAFE_KEYS = new Set(['__proto__', 'constructor', 'prototype']);\n\n/**\n * Creates a deep copy of an object or array\n * @param obj - The object to clone\n * @returns Deep cloned object\n * @since 1.9.0\n */\nexport function deepClone<T>(obj: T): T {\n if (obj === null || typeof obj !== \"object\") {\n return obj;\n }\n\n if (obj instanceof Date) {\n return new Date(obj.getTime()) as T;\n }\n\n if (Array.isArray(obj)) {\n return obj.map(item => deepClone(item)) as T;\n }\n\n const cloned = {} as T;\n for (const key in obj) {\n if (Object.prototype.hasOwnProperty.call(obj, key) && !UNSAFE_KEYS.has(key)) {\n cloned[key] = deepClone(obj[key]);\n }\n }\n\n return cloned;\n}\n","/**\n * This file is part of helpers4.\n * Copyright (C) 2025 baxyz\n * SPDX-License-Identifier: LGPL-3.0-or-later\n */\n\n/**\n * Checks if a value is a plain object.\n *\n * A plain object is created by `{}`, `new Object()`, or `Object.create(null)`.\n * Returns `false` for arrays, Date, Map, Set, RegExp, class instances, etc.\n *\n * @param value - The value to check\n * @returns True if value is a plain object\n * @example\n * isPlainObject({}) // => true\n * isPlainObject({ a: 1 }) // => true\n * isPlainObject(new Date()) // => false\n * isPlainObject([]) // => false\n * isPlainObject(null) // => false\n * @since 2.0.0\n */\nexport function isPlainObject(value: unknown): value is Record<string, unknown> {\n if (value === null || typeof value !== 'object') return false;\n const proto = Object.getPrototypeOf(value);\n return proto === Object.prototype || proto === null;\n}\n","/**\n * This file is part of helpers4.\n * Copyright (C) 2025 baxyz\n * SPDX-License-Identifier: LGPL-3.0-or-later\n */\n\nimport { isPlainObject } from '../type/isPlainObject';\n\nconst UNSAFE_KEYS = new Set(['__proto__', 'constructor', 'prototype']);\n\n/**\n * Merges two or more objects deeply\n * @param target - The target object\n * @param sources - The source objects to merge\n * @returns The merged object\n * @since 1.9.0\n */\nexport function deepMerge<T extends Record<string, unknown>>(target: T, ...sources: Record<string, unknown>[]): T;\nexport function deepMerge(target: undefined, ...sources: Record<string, unknown>[]): undefined;\nexport function deepMerge(target: null, ...sources: Record<string, unknown>[]): null;\nexport function deepMerge<T extends Record<string, unknown>>(target: T | undefined | null, ...sources: Record<string, unknown>[]): T | undefined | null {\n if (target === undefined || target === null) return target;\n if (!sources.length) return target;\n const source = sources.shift();\n\n if (!source) return deepMerge(target, ...sources);\n\n for (const key in source) {\n if (!Object.hasOwn(source, key) || UNSAFE_KEYS.has(key)) continue;\n\n const targetValue = target[key];\n const sourceValue = source[key];\n\n if (isPlainObject(targetValue) && isPlainObject(sourceValue)) {\n (target as Record<string, unknown>)[key] = deepMerge(targetValue as Record<string, unknown>, sourceValue as Record<string, unknown>);\n } else if (sourceValue !== undefined) {\n (target as Record<string, unknown>)[key] = sourceValue;\n }\n }\n\n return deepMerge(target, ...sources);\n}\n","/**\n * This file is part of helpers4.\n * Copyright (C) 2025 baxyz\n * SPDX-License-Identifier: LGPL-3.0-or-later\n */\n\nimport { isPlainObject } from '../type/isPlainObject';\n\n/**\n * Recursive structural array equality.\n *\n * Two arrays are equal when they have the same length and each pair of\n * elements at the same index is structurally equal:\n * - Arrays recurse with `equalsDeep`.\n * - Plain objects recurse key-by-key with structural comparison.\n * - `Date` instances are compared by their epoch value.\n * - All other values use strict equality (`===`), which means `NaN !== NaN`\n * and special objects (Map, Set, RegExp, Promise, class instances\\u2026) are\n * compared by reference.\n *\n * For positional one-level comparison use {@link equalsShallow}. For\n * order-independent comparison use {@link equalsUnordered}.\n *\n * @param arrA - First array to compare\n * @param arrB - Second array to compare\n * @returns `true` if arrays are deeply equal, `false` otherwise.\n * @since 2.0.0\n */\nexport function equalsDeep<T>(arrA: readonly T[], arrB: readonly T[]): boolean {\n if (arrA === arrB) {\n return true;\n }\n if (!Array.isArray(arrA) || !Array.isArray(arrB)) {\n return false;\n }\n if (arrA.length !== arrB.length) {\n return false;\n }\n for (let i = 0; i < arrA.length; i++) {\n if (!valuesEqual(arrA[i], arrB[i])) {\n return false;\n }\n }\n return true;\n}\n\nfunction valuesEqual(a: unknown, b: unknown): boolean {\n if (a === b) {\n return true;\n }\n if (Array.isArray(a) && Array.isArray(b)) {\n return equalsDeep(a, b);\n }\n if (a instanceof Date && b instanceof Date) {\n return a.getTime() === b.getTime();\n }\n if (isPlainObject(a) && isPlainObject(b)) {\n const ao = a as Record<string, unknown>;\n const bo = b as Record<string, unknown>;\n const keysA = Object.keys(ao);\n if (keysA.length !== Object.keys(bo).length) {\n return false;\n }\n for (const key of keysA) {\n if (!Object.prototype.hasOwnProperty.call(bo, key)) {\n return false;\n }\n if (!valuesEqual(ao[key], bo[key])) {\n return false;\n }\n }\n return true;\n }\n return false;\n}\n","/**\n * This file is part of helpers4.\n * Copyright (C) 2025 baxyz\n * SPDX-License-Identifier: LGPL-3.0-or-later\n */\n\nimport { ensureDate } from './ensureDate';\nimport type { DateLike } from './types';\n\n/**\n * Checks if a timestamp is likely in seconds (Java/Unix style) vs milliseconds (JavaScript style)\n * @param timestamp - The timestamp to check\n * @returns True if timestamp appears to be in seconds\n * @since 1.9.0\n */\nexport function isTimestampInSeconds(timestamp: number): boolean {\n // Timestamps before year 2001 in milliseconds are less than 10^10\n // Use Math.abs to correctly handle negative timestamps (before epoch)\n return Math.abs(timestamp) < 10000000000;\n}\n\n/**\n * Converts a timestamp to JavaScript milliseconds format\n * @param timestamp - The timestamp (in seconds or milliseconds)\n * @returns Timestamp in milliseconds\n * @since 1.9.0\n */\nexport function normalizeTimestamp(timestamp: number): number {\n return isTimestampInSeconds(timestamp) ? timestamp * 1000 : timestamp;\n}\n\n/**\n * Converts a date to a timestamp in **seconds** (epoch seconds).\n *\n * Use this when sending a date to a backend API that expects seconds\n * (e.g. Java `Instant.getEpochSecond()`, Python `time.time()`).\n *\n * @param date - The date to convert\n * @returns Seconds since the Unix epoch, or `null` for invalid input\n * @example\n * toSeconds('2025-01-19T12:00:00Z') // => 1737288000\n * toSeconds(null) // => null\n *\n * @since 2.0.0\n */\nexport function toSeconds(date: DateLike): number | null {\n const d = ensureDate(date);\n if (!d) return null;\n return Math.floor(d.getTime() / 1000);\n}\n\n/**\n * Converts a date to a timestamp in **milliseconds** (epoch millis).\n *\n * Use this when you need a plain number from a `DateLike` value\n * (e.g. for `Date.now()` comparisons, localStorage, or JS-native APIs).\n *\n * @param date - The date to convert\n * @returns Milliseconds since the Unix epoch, or `null` for invalid input\n * @example\n * toMillis('2025-01-19T12:00:00Z') // => 1737288000000\n * toMillis(null) // => null\n *\n * @since 2.0.0\n */\nexport function toMillis(date: DateLike): number | null {\n const d = ensureDate(date);\n if (!d) return null;\n return d.getTime();\n}\n\n/**\n * Creates a `Date` from a timestamp in **seconds**.\n *\n * Use this when receiving a timestamp from a backend that sends seconds\n * (e.g. Java `Instant.getEpochSecond()`). No heuristic — the input is\n * always treated as seconds.\n *\n * @param seconds - Seconds since the Unix epoch\n * @returns A valid `Date`, or `null` for `NaN` / non-finite input\n * @example\n * fromSeconds(1737288000) // => Date('2025-01-19T12:00:00Z')\n * fromSeconds(0) // => Date('1970-01-01T00:00:00Z')\n * fromSeconds(NaN) // => null\n *\n * @since 2.0.0\n */\nexport function fromSeconds(seconds: number): Date | null {\n if (!Number.isFinite(seconds)) return null;\n return new Date(seconds * 1000);\n}\n\n/**\n * Creates a `Date` from a timestamp in **milliseconds**.\n *\n * Use this when receiving a timestamp from a JS-native source\n * (e.g. `Date.now()`, `performance.timeOrigin`). No heuristic — the\n * input is always treated as milliseconds.\n *\n * @param ms - Milliseconds since the Unix epoch\n * @returns A valid `Date`, or `null` for `NaN` / non-finite input\n * @example\n * fromMillis(1737288000000) // => Date('2025-01-19T12:00:00Z')\n * fromMillis(0) // => Date('1970-01-01T00:00:00Z')\n * fromMillis(NaN) // => null\n *\n * @since 2.0.0\n */\nexport function fromMillis(ms: number): Date | null {\n if (!Number.isFinite(ms)) return null;\n return new Date(ms);\n}\n","/**\n * This file is part of helpers4.\n * Copyright (C) 2025 baxyz\n * SPDX-License-Identifier: LGPL-3.0-or-later\n */\n\nimport { normalizeTimestamp } from './timestamp';\nimport type { DateLike, EpochMilliseconds } from './types';\n\n/**\n * Type guard that checks whether a value satisfies the {@link EpochMilliseconds}\n * structural interface (e.g. `Temporal.Instant`, `Temporal.ZonedDateTime`).\n */\nfunction hasEpochMilliseconds(value: unknown): value is EpochMilliseconds {\n return (\n typeof value === 'object' &&\n value !== null &&\n 'epochMilliseconds' in value &&\n typeof (value as EpochMilliseconds).epochMilliseconds === 'number'\n );\n}\n\n/**\n * Safely converts a date-like value to a valid `Date` object, or returns `null`.\n *\n * Accepts `Date`, timestamps (seconds or milliseconds, auto-detected), date strings,\n * and objects with an `epochMilliseconds` property (e.g. `Temporal.Instant`,\n * `Temporal.ZonedDateTime`).\n * Returns `null` for `null`, `undefined`, empty strings, `0`, and any value that\n * produces an invalid `Date`.\n *\n * This is the date equivalent of {@link ensureArray} — it normalizes flexible\n * input into a guaranteed type (or a safe fallback).\n *\n * @param input - A date-like value to convert\n * @returns A valid `Date` object, or `null` if the input is invalid\n * @example\n * ensureDate('2025-01-19T12:00:00Z') // => Date\n * ensureDate(1737290400) // => Date (from Unix seconds)\n * ensureDate(1737290400000) // => Date (from milliseconds)\n * ensureDate(new Date()) // => Date (same reference)\n * ensureDate(null) // => null\n * ensureDate('invalid') // => null\n *\n * @see {@link DateLike} for the accepted input types\n * @since 2.0.0\n */\nexport function ensureDate(input: DateLike | null | undefined): Date | null {\n if (input === null || input === undefined || input === '' || input === 0) {\n return null;\n }\n\n if (input instanceof Date) {\n return isNaN(input.getTime()) ? null : input;\n }\n\n if (hasEpochMilliseconds(input)) {\n const date = new Date(input.epochMilliseconds);\n return isNaN(date.getTime()) ? null : date;\n }\n\n if (typeof input === 'number') {\n const normalized = normalizeTimestamp(input);\n const date = new Date(normalized);\n return isNaN(date.getTime()) ? null : date;\n }\n\n if (typeof input === 'string') {\n const date = new Date(input);\n return isNaN(date.getTime()) ? null : date;\n }\n\n return null;\n}\n","/**\n * This file is part of helpers4.\n * Copyright (C) 2025 baxyz\n * SPDX-License-Identifier: LGPL-3.0-or-later\n */\n\nimport { ensureDate } from './ensureDate';\nimport type { DateLike } from './types';\n\n/**\n * Options for date comparison\n * @since 2.0.0\n */\nexport interface DateCompareOptions {\n /**\n * Comparison precision\n * - 'milliseconds': Compare all including milliseconds (default)\n * - 'seconds': Ignore milliseconds, compare to the second\n * - 'minutes': Ignore seconds and milliseconds, compare to the minute\n * - 'hours': Ignore minutes, seconds and milliseconds, compare to the hour\n * - 'days': Compare only the date part (ignore time completely)\n * - 'months': Compare only year and month\n * - 'years': Compare only year\n */\n precision?: 'milliseconds' | 'seconds' | 'minutes' | 'hours' | 'days' | 'months' | 'years';\n}\n\n/**\n * Comparison of two dates.\n *\n * Accepts any {@link DateLike} input (Date, timestamp, or date string).\n *\n * @param dateA - First date to compare\n * @param dateB - Second date to compare\n * @param options - Comparison options\n * @returns `true` if dates are identical according to the specified precision, `false` otherwise\n * @example\n * compare('2025-01-19', new Date('2025-01-19T08:00:00Z'), { precision: 'days' })\n * // => true\n * @since 2.0.0\n */\nexport function compare(dateA: DateLike, dateB: DateLike, options: DateCompareOptions = {}): boolean {\n const { precision = 'milliseconds' } = options;\n\n const a = ensureDate(dateA);\n const b = ensureDate(dateB);\n\n // Any invalid → not equal\n if (a === null || b === null) {\n return false;\n }\n\n switch (precision) {\n case 'years':\n return a.getFullYear() === b.getFullYear();\n\n case 'months':\n return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth();\n\n case 'days':\n return a.toDateString() === b.toDateString();\n\n case 'hours': {\n const hoursA = Math.floor(a.getTime() / (1000 * 60 * 60));\n const hoursB = Math.floor(b.getTime() / (1000 * 60 * 60));\n return hoursA === hoursB;\n }\n\n case 'minutes': {\n const minutesA = Math.floor(a.getTime() / (1000 * 60));\n const minutesB = Math.floor(b.getTime() / (1000 * 60));\n return minutesA === minutesB;\n }\n\n case 'seconds': {\n const secondsA = Math.floor(a.getTime() / 1000);\n const secondsB = Math.floor(b.getTime() / 1000);\n return secondsA === secondsB;\n }\n\n case 'milliseconds':\n default:\n return a.getTime() === b.getTime();\n }\n}\n","/**\n * This file is part of helpers4.\n * Copyright (C) 2025 baxyz\n * SPDX-License-Identifier: LGPL-3.0-or-later\n */\n\n/**\n * Determines if a value is a special object that should not have its properties compared deeply.\n * Special objects include: Date, Function, Promise, Observable, RegExp, Error, Map, Set, WeakMap, WeakSet, etc.\n * \n * @param value - The value to check\n * @returns `true` if the value is a special object, `false` otherwise\n * @since 2.0.0\n */\nexport function isSpecialObject(value: unknown): boolean {\n if (value === null || value === undefined) {\n return false;\n }\n\n // Functions are special objects\n if (typeof value === 'function') {\n return true;\n }\n\n // Non-objects are not special\n if (typeof value !== 'object') {\n return false;\n }\n\n // Arrays are not considered special objects for our purposes\n if (Array.isArray(value)) {\n return false;\n }\n\n // Check for built-in special objects\n if (\n value instanceof Date ||\n value instanceof Promise ||\n value instanceof RegExp ||\n value instanceof Error ||\n value instanceof Map ||\n value instanceof Set ||\n value instanceof WeakMap ||\n value instanceof WeakSet ||\n value instanceof ArrayBuffer ||\n value instanceof DataView\n ) {\n return true;\n }\n\n // Check for Observable (RxJS or similar)\n if (value.constructor?.name === 'Observable') {\n return true;\n }\n\n // Check for DOM elements (if in browser environment)\n if (typeof HTMLElement !== 'undefined' && value instanceof HTMLElement) {\n return true;\n }\n\n // Check for other built-in types by constructor name\n const constructorName = value.constructor?.name;\n if (constructorName && [\n 'Buffer', // Node.js Buffer\n 'URL', // Web URL API\n 'URLSearchParams',\n 'File', // Web File API\n 'Blob', // Web Blob API\n 'FormData', // Web FormData API\n 'Headers', // Web Headers API\n 'Request', // Web Request API\n 'Response', // Web Response API\n 'EventTarget', // Web EventTarget API\n 'Symbol' // Symbol objects\n ].includes(constructorName)) {\n return true;\n }\n\n return false;\n}\n","/**\n * This file is part of helpers4.\n * Copyright (C) 2025 baxyz\n * SPDX-License-Identifier: LGPL-3.0-or-later\n */\n\nimport { equalsDeep as arrayDeepEquals } from '../array/equalsDeep';\nimport { compare as compareDate } from '../date/compare';\nimport { isSpecialObject } from '../type/isSpecialObject';\n\n/**\n * Per-key diff result. Each entry tells what differs between the two\n * objects:\n * - `\"onlyA\"` \\u2014 the key exists only on the first input.\n * - `\"onlyB\"` \\u2014 the key exists only on the second input.\n * - `false` \\u2014 both inputs have the key but the values differ.\n * - nested `DiffResult` \\u2014 plain-object values that differ deeper.\n *\n * @since 2.0.0\n */\nexport interface DiffResult {\n [key: string]: 'onlyA' | 'onlyB' | false | DiffResult;\n}\n\n/**\n * Options for {@link diff}.\n * @since 2.0.0\n */\nexport interface DiffOptions {\n /**\n * Maximum recursion depth for nested plain objects. Defaults to\n * `Infinity` (full structural diff).\n *\n * - `0` compares values at the top-level only with strict equality.\n * - `1` compares the first level of nested objects with strict equality.\n * - `Infinity` recurses through every plain-object level.\n *\n * Arrays, `Date` and special objects (Map, Set, RegExp\\u2026) are always\n * leaf-compared (via {@link arrayDeepEquals}, epoch comparison, or\n * reference equality respectively) regardless of `depth`.\n */\n depth?: number;\n}\n\n/**\n * Structural object diff.\n *\n * Returns `true` when both inputs are deeply equal, otherwise a\n * {@link DiffResult} describing the differences key by key.\n *\n * Comparison rules:\n * - Same reference \\u2192 `true`.\n * - Either side is `null`/`undefined` (and not both) \\u2192 `false`.\n * - Both `Date` \\u2192 epoch comparison.\n * - Both arrays \\u2192 compared with `array/equalsDeep` (leaf, no diff drill-down).\n * - Special objects (Map, Set, RegExp, Promise, class instances\\u2026) \\u2192 reference equality.\n * - Plain objects \\u2192 key-by-key, recursing up to `options.depth` levels.\n * - Mixed types (e.g. array vs object, Date vs object) \\u2192 `false`.\n *\n * For a boolean wrapper see {@link equalsDeep} from this category.\n * For a one-level boolean check see {@link equalsShallow} from this category.\n *\n * @param objA - First value (object, `null`, or `undefined`).\n * @param objB - Second value (object, `null`, or `undefined`).\n * @param options - See {@link DiffOptions}.\n * @returns `true` when equal, otherwise a {@link DiffResult}, or `false` for incompatible types.\n * @since 2.0.0\n */\nexport function diff(\n objA: object | undefined | null,\n objB: object | undefined | null,\n options: DiffOptions = {},\n): true | false | DiffResult {\n const depth = options.depth ?? Infinity;\n\n // Quick reference equality check\n if (objA === objB) {\n return true;\n }\n\n // Handle null/undefined cases\n if (objA == null || objB == null) {\n return false;\n }\n\n // Handle Date objects\n if (objA instanceof Date && objB instanceof Date) {\n return compareDate(objA, objB);\n }\n\n // Handle Arrays at root level\n if (Array.isArray(objA) && Array.isArray(objB)) {\n return arrayDeepEquals(objA, objB);\n }\n if (Array.isArray(objA) || Array.isArray(objB)) {\n return false;\n }\n\n // Handle special objects - compare by reference only\n if (isSpecialObject(objA) || isSpecialObject(objB)) {\n return objA === objB;\n }\n\n // Plain object detailed comparison\n return diffPlainObjects(objA, objB, depth);\n}\n\nfunction diffPlainObjects(\n objA: object,\n objB: object,\n depth: number,\n): true | DiffResult {\n const keys = Array.from(new Set([...Object.keys(objA), ...Object.keys(objB)]));\n const differences: DiffResult = Object.create(null) as DiffResult;\n\n for (const key of keys) {\n const hasA = Object.prototype.hasOwnProperty.call(objA, key);\n const hasB = Object.prototype.hasOwnProperty.call(objB, key);\n\n if (!hasA && hasB) {\n differences[key] = 'onlyB';\n continue;\n }\n if (hasA && !hasB) {\n differences[key] = 'onlyA';\n continue;\n }\n\n const valueA = (objA as Record<string, unknown>)[key];\n const valueB = (objB as Record<string, unknown>)[key];\n\n if (valueA === valueB) {\n continue;\n }\n\n if (Array.isArray(valueA) && Array.isArray(valueB)) {\n if (!arrayDeepEquals(valueA, valueB)) {\n differences[key] = false;\n }\n continue;\n }\n if (Array.isArray(valueA) || Array.isArray(valueB)) {\n differences[key] = false;\n continue;\n }\n\n if (valueA instanceof Date && valueB instanceof Date) {\n if (!compareDate(valueA, valueB)) {\n differences[key] = false;\n }\n continue;\n }\n if (valueA instanceof Date || valueB instanceof Date) {\n differences[key] = false;\n continue;\n }\n\n if (isSpecialObject(valueA) || isSpecialObject(valueB)) {\n // valueA === valueB already handled above, so any special-object pair\n // that reaches here must differ.\n differences[key] = false;\n continue;\n }\n\n const bothPlainObjects =\n valueA !== null && valueB !== null &&\n typeof valueA === 'object' && typeof valueB === 'object';\n\n if (!bothPlainObjects) {\n differences[key] = false;\n continue;\n }\n\n if (depth <= 0) {\n // depth budget exhausted \\u2192 strict-equal nested objects only match by reference\n differences[key] = false;\n continue;\n }\n\n const nested = diffPlainObjects(valueA as object, valueB as object, depth - 1);\n if (nested !== true) {\n differences[key] = nested;\n }\n }\n\n return Object.keys(differences).length > 0 ? differences : true;\n}\n","/**\n * This file is part of helpers4.\n * Copyright (C) 2025 baxyz\n * SPDX-License-Identifier: LGPL-3.0-or-later\n */\n\nimport { diff } from './diff';\n\n/**\n * Recursive structural object equality.\n *\n * Boolean wrapper around {@link diff} \\u2014 returns `true` when the two values\n * are deeply equal according to the same rules. Use this when you only\n * need a yes/no answer; use {@link diff} when you also need to know\n * *what* differs.\n *\n * For a one-level boolean check use {@link equalsShallow}.\n *\n * @param objA - First value (object, `null`, or `undefined`).\n * @param objB - Second value (object, `null`, or `undefined`).\n * @returns `true` if both inputs are deeply equal, `false` otherwise.\n * @since 2.0.0\n */\nexport function equalsDeep(\n objA: object | undefined | null,\n objB: object | undefined | null,\n): boolean {\n return diff(objA, objB) === true;\n}\n","/**\n * This file is part of helpers4.\n * Copyright (C) 2025 baxyz\n * SPDX-License-Identifier: LGPL-3.0-or-later\n */\n\n/**\n * One-level (shallow) object equality.\n *\n * Two objects are equal when they share the exact same set of own\n * enumerable string keys and each pair of values satisfies strict equality\n * (`===`). No recursion: nested objects/arrays are compared by reference.\n *\n * Falls back to strict equality when either input is `null`, `undefined`\n * or not an object \\u2014 so primitives match if and only if they are `===`.\n * Arrays are not supported; they always return `false` (unless identical\n * references). Use `array/equalsShallow` instead.\n *\n * For recursive structural comparison use {@link equalsDeep}. For a diff\n * structure use {@link diff}.\n *\n * @param objA - First value to compare\n * @param objB - Second value to compare\n * @returns `true` if values are shallowly equal, `false` otherwise.\n * @since 2.0.0\n */\nexport function equalsShallow(objA: unknown, objB: unknown): boolean {\n if (objA === objB) {\n return true;\n }\n if (\n objA === null || objB === null ||\n typeof objA !== 'object' || typeof objB !== 'object'\n ) {\n return false;\n }\n const a = objA as Record<string, unknown>;\n const b = objB as Record<string, unknown>;\n if (Array.isArray(objA) || Array.isArray(objB)) {\n return false;\n }\n const keysA = Object.keys(a);\n if (keysA.length !== Object.keys(b).length) {\n return false;\n }\n for (const key of keysA) {\n if (!Object.prototype.hasOwnProperty.call(b, key) || a[key] !== b[key]) {\n return false;\n }\n }\n return true;\n}\n","/**\n * This file is part of helpers4.\n * Copyright (C) 2025 baxyz\n * SPDX-License-Identifier: LGPL-3.0-or-later\n */\n\n/**\n * Gets a value from an object using a dot-notated path\n * @param obj - The object to get value from\n * @param path - The dot-notated path (e.g., 'a.b.c')\n * @param defaultValue - Default value if path doesn't exist\n * @returns The value at the path or default value\n * @since 1.9.0\n */\nexport function get<T = unknown>(obj: unknown, path: string, defaultValue?: T): T | undefined {\n const keys = path.split('.');\n let result: unknown = obj;\n\n for (const key of keys) {\n if (result == null || typeof result !== 'object') {\n return defaultValue;\n }\n result = (result as Record<string, unknown>)[key];\n }\n\n return result !== undefined ? result as T | undefined : defaultValue;\n}\n","/**\n * This file is part of helpers4.\n * Copyright (C) 2025 baxyz\n * SPDX-License-Identifier: LGPL-3.0-or-later\n */\n\n/**\n * Groups an array of items by a key derived from each item.\n *\n * A thin, typed wrapper around `Object.groupBy` (ES2024) that works on\n * older targets and provides stricter return-type inference.\n *\n * @param items - The array to group\n * @param keyFn - Function that returns the group key for each item\n * @returns A record mapping each key to the array of items with that key\n * @example\n * groupBy([1, 2, 3, 4], n => n % 2 === 0 ? 'even' : 'odd')\n * // => { odd: [1, 3], even: [2, 4] }\n * @since 2.0.0\n */\nexport function groupBy<T, K extends PropertyKey>(\n items: readonly T[],\n keyFn: (item: T) => K\n): Partial<Record<K, T[]>> {\n const result = {} as Partial<Record<K, T[]>>;\n for (const item of items) {\n const key = keyFn(item);\n if (result[key] === undefined) {\n result[key] = [];\n }\n (result[key] as T[]).push(item);\n }\n return result;\n}\n","/**\n * This file is part of helpers4.\n * Copyright (C) 2025 baxyz\n * SPDX-License-Identifier: LGPL-3.0-or-later\n */\n\n/**\n * Returns a new object with keys and values swapped.\n * If multiple keys share the same value, the last one wins.\n *\n * @param obj - The object whose keys and values are to be swapped\n * @returns A new object with values as keys and original keys as values\n * @example\n * invert({ a: 'x', b: 'y', c: 'z' })\n * // => { x: 'a', y: 'b', z: 'c' }\n *\n * invert({ one: 1, two: 2 })\n * // => { 1: 'one', 2: 'two' }\n * @since 2.0.0\n */\nexport function invert<K extends string, V extends PropertyKey>(obj: Record<K, V>): Record<V, K> {\n const result = {} as Record<V, K>;\n for (const key in obj) {\n if (Object.prototype.hasOwnProperty.call(obj, key)) {\n result[obj[key as K]] = key as unknown as K;\n }\n }\n return result;\n}\n","/**\n * This file is part of helpers4.\n * Copyright (C) 2025 baxyz\n * SPDX-License-Identifier: LGPL-3.0-or-later\n */\n\n/**\n * Checks if a plain object has no own enumerable string-keyed properties.\n *\n * Symbol-keyed properties are not counted. Use `Object.getOwnPropertySymbols`\n * separately if symbol keys matter for your use case.\n *\n * @param value - The object to check\n * @returns `true` if the object has no own enumerable string-keyed properties\n * @example\n * isEmpty({}) // => true\n * isEmpty({ a: 1 }) // => false\n * isEmpty({ a: undefined }) // => false (key exists)\n * @since 2.0.3\n */\nexport function isEmpty(value: Record<PropertyKey, unknown>): boolean {\n return Object.keys(value).length === 0;\n}\n","/**\n * This file is part of helpers4.\n * Copyright (C) 2025 baxyz\n * SPDX-License-Identifier: LGPL-3.0-or-later\n */\n\n/**\n * Checks if a plain object has at least one own enumerable string-keyed property.\n *\n * Symbol-keyed properties are not counted. Use `Object.getOwnPropertySymbols`\n * separately if symbol keys matter for your use case.\n *\n * @param value - The object to check\n * @returns `true` if the object has at least one own enumerable string-keyed property\n * @example\n * isNonEmpty({ a: 1 }) // => true\n * isNonEmpty({}) // => false\n * @since 2.0.3\n */\nexport function isNonEmpty(value: Record<PropertyKey, unknown>): boolean {\n return Object.keys(value).length > 0;\n}\n","/**\n * This file is part of helpers4.\n * Copyright (C) 2025 baxyz\n * SPDX-License-Identifier: LGPL-3.0-or-later\n */\n\n/**\n * Transforms the values and/or keys of a plain object in a single pass.\n *\n * Both callbacks are optional and default to identity (no transformation).\n * When `mapValue` is omitted the original values are preserved;\n * when `mapKey` is omitted the original keys are preserved.\n *\n * Note: if two different keys map to the same output key the last one wins\n * (insertion order).\n *\n * @param obj - The source object\n * @param mapValue - Callback called with `(value, key)` for each entry.\n * Defaults to identity.\n * @param mapKey - Callback called with `(key, value)` for each entry.\n * Defaults to identity.\n * @returns A new object with transformed keys and/or values\n * @example\n * map({ a: 1, b: 2 }, v => v * 10)\n * // => { a: 10, b: 20 }\n *\n * map({ a: 1, b: 2 }, undefined, k => k.toUpperCase())\n * // => { A: 1, B: 2 }\n *\n * map({ a: 1, b: 2 }, v => v * 10, k => k.toUpperCase())\n * // => { A: 10, B: 20 }\n * @since 2.0.0\n */\nexport function map<\n TObj extends Record<string, unknown>,\n TVal = TObj[keyof TObj],\n TKey extends PropertyKey = keyof TObj,\n>(\n obj: TObj,\n mapValue?: (value: TObj[keyof TObj], key: keyof TObj) => TVal,\n mapKey?: (key: keyof TObj, value: TObj[keyof TObj]) => TKey\n): Record<TKey, TVal> {\n const result = {} as Record<TKey, TVal>;\n for (const key of Object.keys(obj) as (keyof TObj)[]) {\n const value = obj[key];\n const newKey = mapKey ? mapKey(key, value) : (key as unknown as TKey);\n const newVal = mapValue ? mapValue(value, key) : (value as unknown as TVal);\n result[newKey] = newVal;\n }\n return result;\n}\n","/**\n * This file is part of helpers4.\n * Copyright (C) 2025 baxyz\n * SPDX-License-Identifier: LGPL-3.0-or-later\n */\n\n/**\n * Creates a new object without the specified keys.\n * @param obj - The source object\n * @param keys - The keys to omit\n * @returns A new object without the omitted keys\n * @example\n * omit({ a: 1, b: 2, c: 3 }, ['b'])\n * // => { a: 1, c: 3 }\n * @since 2.0.0\n */\nexport function omit<T extends Record<string, unknown>, K extends keyof T>(obj: T, keys: readonly K[]): Omit<T, K>;\nexport function omit(obj: undefined, keys: readonly string[]): undefined;\nexport function omit(obj: null, keys: readonly string[]): null;\nexport function omit<T extends Record<string, unknown>, K extends keyof T>(\n obj: T | undefined | null,\n keys: readonly K[]\n): Omit<T, K> | undefined | null {\n if (obj === undefined || obj === null) return obj;\n const result = { ...obj };\n for (const key of keys) {\n delete result[key];\n }\n return result as Omit<T, K>;\n}\n","/**\n * This file is part of helpers4.\n * Copyright (C) 2025 baxyz\n * SPDX-License-Identifier: LGPL-3.0-or-later\n */\n\n/**\n * Creates a new object with only the specified keys.\n * @param obj - The source object\n * @param keys - The keys to pick\n * @returns A new object with only the picked keys\n * @example\n * pick({ a: 1, b: 2, c: 3 }, ['a', 'c'])\n * // => { a: 1, c: 3 }\n * @since 2.0.0\n */\nexport function pick<T extends Record<string, unknown>, K extends keyof T>(obj: T, keys: readonly K[]): Pick<T, K>;\nexport function pick(obj: undefined, keys: readonly string[]): undefined;\nexport function pick(obj: null, keys: readonly string[]): null;\nexport function pick<T extends Record<string, unknown>, K extends keyof T>(\n obj: T | undefined | null,\n keys: readonly K[]\n): Pick<T, K> | undefined | null {\n if (obj === undefined || obj === null) return obj;\n const result = {} as Pick<T, K>;\n for (const key of keys) {\n if (Object.prototype.hasOwnProperty.call(obj, key)) {\n result[key] = obj[key];\n }\n }\n return result;\n}\n","/**\n * This file is part of helpers4.\n * Copyright (C) 2025 baxyz\n * SPDX-License-Identifier: LGPL-3.0-or-later\n */\n\n/**\n * Checks if a value is null or undefined (nullish).\n * @param value - The value to check\n * @returns True if value is null or undefined\n * @example\n * isNullish(null) // => true\n * isNullish(undefined) // => true\n * isNullish(0) // => false\n * isNullish('') // => false\n *\n * @see {@link isDefined} for the inverse check (`!isNullish`)\n * @see {@link isFalsy} to also catch `0`, `\"\"`, `false`, and `NaN`\n * @since 2.0.0\n */\nexport function isNullish(value: unknown): value is null | undefined {\n return value === null || value === undefined;\n}\n","/**\n * This file is part of helpers4.\n * Copyright (C) 2025 baxyz\n * SPDX-License-Identifier: LGPL-3.0-or-later\n */\n\nimport { isNullish } from '../type/isNullish';\n\n/**\n * Remove null and undefined values from an object.\n *\n * @param obj an object\n * @returns A shallow copy of the object without null or undefined values\n * @since 1.0.0\n */\nexport function removeUndefinedNull<T extends Record<string, string | number | boolean | null | undefined>>(obj: T): Partial<T>;\n\n/**\n * Remove null and undefined values from an object.\n *\n * @param obj a null object\n * @returns null\n * @since 1.0.0\n */\nexport function removeUndefinedNull(obj: null): null;\n\n/**\n * Remove null and undefined values from an object.\n *\n * @param obj an undefined object\n * @returns undefined\n * @since 1.0.0\n */\nexport function removeUndefinedNull(obj: undefined): undefined;\n\n/**\n * Remove null and undefined values from an object.\n *\n * @param obj an object\n * @returns A shallow copy of the object without null or undefined values\n * @since 1.0.0\n */\nexport function removeUndefinedNull<T extends Record<string, string | number | boolean | null | undefined>>(obj: T | null | undefined): Partial<T> | null | undefined {\n if (!obj) return obj;\n const result: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(obj)) {\n if (!isNullish(v)) { result[k] = v; }\n }\n return result as Partial<T>;\n}\n\n","/**\n * This file is part of helpers4.\n * Copyright (C) 2025 baxyz\n * SPDX-License-Identifier: LGPL-3.0-or-later\n */\n\n/**\n * Parses a JSON string, returning `null` (or a fallback) on any parse failure.\n *\n * Unlike `JSON.parse`, this never throws. Invalid JSON strings and other\n * parsing edge-cases resolve to `null` or the provided `fallback`.\n *\n * @param input - The JSON string to parse.\n * @param fallback - Value returned on failure. Defaults to `null` when omitted.\n * @returns The parsed value typed as `T`, or `fallback` on failure.\n * @example\n * safeJsonParse<{ a: number }>('{\"a\":1}') // => { a: 1 }\n * safeJsonParse('invalid') // => null\n * safeJsonParse('invalid', []) // => []\n * @since 2.0.0\n */\nexport function safeJsonParse<T>(input: string): T | null;\nexport function safeJsonParse<T>(input: string, fallback: T): T;\nexport function safeJsonParse<T>(input: string, fallback?: T): T | null {\n try {\n return JSON.parse(input) as T;\n } catch {\n // Distinguish \"no fallback provided\" (→ null) from \"fallback is undefined\"\n // (→ undefined as T) to keep runtime behaviour aligned with the overloads.\n return arguments.length < 2 ? null : (fallback as T);\n }\n}\n","/**\n * This file is part of helpers4.\n * Copyright (C) 2025 baxyz\n * SPDX-License-Identifier: LGPL-3.0-or-later\n */\n\nconst UNSAFE_KEYS = new Set(['__proto__', 'constructor', 'prototype']);\n\n/**\n * Sets a value in an object using a dot-notated path\n * @param obj - The object to set value in\n * @param path - The dot-notated path (e.g., 'a.b.c')\n * @param value - The value to set\n * @returns The modified object\n * @since 1.9.0\n */\nexport function set(obj: Record<string, unknown>, path: string, value: unknown): Record<string, unknown> {\n const keys = path.split('.');\n\n if (keys.some((k) => UNSAFE_KEYS.has(k))) return obj;\n\n let current: Record<string, unknown> = obj;\n\n for (let i = 0; i < keys.length - 1; i++) {\n const key = keys[i];\n\n if (!(key in current) || typeof current[key] !== 'object' || current[key] === null) {\n current[key] = {};\n }\n\n current = current[key] as Record<string, unknown>;\n }\n\n current[keys[keys.length - 1]] = value;\n return obj;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../../helpers/object/compact.ts","../../../helpers/object/_unsafeKeys.ts","../../../helpers/object/deepClone.ts","../../../helpers/type/isPlainObject.ts","../../../helpers/object/deepMerge.ts","../../../helpers/array/equalsDeep.ts","../../../helpers/date/timestamp.ts","../../../helpers/date/ensureDate.ts","../../../helpers/date/compare.ts","../../../helpers/type/isSpecialObject.ts","../../../helpers/object/diff.ts","../../../helpers/object/equalsDeep.ts","../../../helpers/object/equalsShallow.ts","../../../helpers/object/_parsePath.ts","../../../helpers/object/get.ts","../../../helpers/object/groupBy.ts","../../../helpers/object/invert.ts","../../../helpers/object/isEmpty.ts","../../../helpers/object/isNonEmpty.ts","../../../helpers/object/map.ts","../../../helpers/object/omit.ts","../../../helpers/object/pick.ts","../../../helpers/type/isNullish.ts","../../../helpers/object/removeUndefinedNull.ts","../../../helpers/object/safeJsonParse.ts","../../../helpers/object/set.ts"],"sourcesContent":["/**\n * This file is part of helpers4.\n * Copyright (C) 2025 baxyz\n * SPDX-License-Identifier: LGPL-3.0-or-later\n */\n\n/**\n * Removes all entries with falsy values (`false`, `null`, `undefined`, `0`, `\"\"`, `NaN`) from an object.\n * @param obj - The source object\n * @returns A new object containing only entries with truthy values\n * @example\n * compact({ a: 1, b: null, c: '', d: 0, e: 'hello' })\n * // => { a: 1, e: 'hello' }\n * @since 2.0.0\n */\nexport function compact<T extends Record<string, unknown>>(obj: T): Partial<T>;\nexport function compact(obj: undefined): undefined;\nexport function compact(obj: null): null;\nexport function compact<T extends Record<string, unknown>>(obj: T | undefined | null): Partial<T> | undefined | null {\n if (obj === undefined || obj === null) return obj;\n const result: Partial<T> = {};\n for (const key of Object.keys(obj) as (keyof T)[]) {\n if (obj[key]) {\n result[key] = obj[key];\n }\n }\n return result;\n}\n","/**\n * This file is part of helpers4.\n * Copyright (C) 2025 baxyz\n * SPDX-License-Identifier: LGPL-3.0-or-later\n */\n\n/**\n * Keys that must never be set on a plain object (prototype-pollution guard).\n * @internal\n */\nexport const UNSAFE_KEYS = new Set(['__proto__', 'constructor', 'prototype']);\n","/**\n * This file is part of helpers4.\n * Copyright (C) 2025 baxyz\n * SPDX-License-Identifier: LGPL-3.0-or-later\n */\n\nimport { UNSAFE_KEYS } from './_unsafeKeys.js';\n\n/**\n * Creates a deep copy of an object or array\n * @param obj - The object to clone\n * @returns Deep cloned object\n * @since 1.9.0\n */\nexport function deepClone<T>(obj: T): T {\n if (obj === null || typeof obj !== \"object\") {\n return obj;\n }\n\n if (obj instanceof Date) {\n return new Date(obj.getTime()) as T;\n }\n\n if (Array.isArray(obj)) {\n return obj.map(item => deepClone(item)) as T;\n }\n\n const cloned = {} as T;\n for (const key in obj) {\n if (Object.prototype.hasOwnProperty.call(obj, key) && !UNSAFE_KEYS.has(key)) {\n cloned[key] = deepClone(obj[key]);\n }\n }\n\n return cloned;\n}\n","/**\n * This file is part of helpers4.\n * Copyright (C) 2025 baxyz\n * SPDX-License-Identifier: LGPL-3.0-or-later\n */\n\n/**\n * Checks if a value is a plain object.\n *\n * A plain object is created by `{}`, `new Object()`, or `Object.create(null)`.\n * Returns `false` for arrays, Date, Map, Set, RegExp, class instances, etc.\n *\n * @param value - The value to check\n * @returns True if value is a plain object\n * @example\n * isPlainObject({}) // => true\n * isPlainObject({ a: 1 }) // => true\n * isPlainObject(new Date()) // => false\n * isPlainObject([]) // => false\n * isPlainObject(null) // => false\n * @since 2.0.0\n */\nexport function isPlainObject(value: unknown): value is Record<string, unknown> {\n if (value === null || typeof value !== 'object') return false;\n const proto = Object.getPrototypeOf(value);\n return proto === Object.prototype || proto === null;\n}\n","/**\n * This file is part of helpers4.\n * Copyright (C) 2025 baxyz\n * SPDX-License-Identifier: LGPL-3.0-or-later\n */\n\nimport { isPlainObject } from '../type/isPlainObject';\nimport { UNSAFE_KEYS } from './_unsafeKeys.js';\n\n// Shared merge logic for a single key (string or symbol).\n// Extracted to avoid duplicating the plain-object / replace / skip logic between\n// the string-key loop (for...in) and the symbol-key loop (getOwnPropertySymbols).\nfunction mergeKey(\n target: Record<PropertyKey, unknown>,\n source: Record<PropertyKey, unknown>,\n key: PropertyKey,\n): void {\n const targetValue = target[key];\n const sourceValue = source[key];\n if (isPlainObject(targetValue) && isPlainObject(sourceValue)) {\n target[key] = deepMerge(\n targetValue as Record<PropertyKey, unknown>,\n sourceValue as Record<PropertyKey, unknown>,\n );\n } else if (sourceValue !== undefined) {\n target[key] = sourceValue;\n }\n}\n\n/**\n * Merges two or more objects deeply.\n *\n * Recursively merges own enumerable properties — both string and symbol keys.\n * Plain objects are merged recursively; all other values (arrays, class instances,\n * primitives, etc.) are replaced by the source value.\n * `undefined` source values do not overwrite existing target values.\n *\n * @param target - The target object (mutated in place)\n * @param sources - One or more source objects to merge into the target\n * @returns The mutated target\n * @since 1.9.0\n */\nexport function deepMerge<T extends Record<PropertyKey, unknown>>(target: T, ...sources: Record<PropertyKey, unknown>[]): T;\nexport function deepMerge(target: undefined, ...sources: Record<PropertyKey, unknown>[]): undefined;\nexport function deepMerge(target: null, ...sources: Record<PropertyKey, unknown>[]): null;\nexport function deepMerge<T extends Record<PropertyKey, unknown>>(target: T | undefined | null, ...sources: Record<PropertyKey, unknown>[]): T | undefined | null {\n if (target === undefined || target === null) return target;\n if (!sources.length) return target;\n const source = sources.shift();\n\n if (!source) return deepMerge(target, ...sources);\n\n const t = target as Record<PropertyKey, unknown>;\n const s = source as Record<PropertyKey, unknown>;\n\n for (const key in source) {\n if (!Object.hasOwn(source, key) || UNSAFE_KEYS.has(key)) continue;\n mergeKey(t, s, key);\n }\n\n for (const sym of Object.getOwnPropertySymbols(source)) {\n if (!Object.prototype.propertyIsEnumerable.call(source, sym)) continue;\n mergeKey(t, s, sym);\n }\n\n return deepMerge(target, ...sources);\n}\n","/**\n * This file is part of helpers4.\n * Copyright (C) 2025 baxyz\n * SPDX-License-Identifier: LGPL-3.0-or-later\n */\n\nimport { isPlainObject } from '../type/isPlainObject';\n\n/**\n * Recursive structural array equality.\n *\n * Two arrays are equal when they have the same length and each pair of\n * elements at the same index is structurally equal:\n * - Arrays recurse with `equalsDeep`.\n * - Plain objects recurse key-by-key with structural comparison.\n * - `Date` instances are compared by their epoch value.\n * - All other values use strict equality (`===`), which means `NaN !== NaN`\n * and special objects (Map, Set, RegExp, Promise, class instances\\u2026) are\n * compared by reference.\n *\n * For positional one-level comparison use {@link equalsShallow}. For\n * order-independent comparison use {@link equalsUnordered}.\n *\n * @param arrA - First array to compare\n * @param arrB - Second array to compare\n * @returns `true` if arrays are deeply equal, `false` otherwise.\n * @since 2.0.0\n */\nexport function equalsDeep<T>(arrA: readonly T[], arrB: readonly T[]): boolean {\n if (arrA === arrB) {\n return true;\n }\n if (!Array.isArray(arrA) || !Array.isArray(arrB)) {\n return false;\n }\n if (arrA.length !== arrB.length) {\n return false;\n }\n for (let i = 0; i < arrA.length; i++) {\n if (!valuesEqual(arrA[i], arrB[i])) {\n return false;\n }\n }\n return true;\n}\n\nfunction valuesEqual(a: unknown, b: unknown): boolean {\n if (a === b) {\n return true;\n }\n if (Array.isArray(a) && Array.isArray(b)) {\n return equalsDeep(a, b);\n }\n if (a instanceof Date && b instanceof Date) {\n return a.getTime() === b.getTime();\n }\n if (isPlainObject(a) && isPlainObject(b)) {\n const ao = a as Record<string, unknown>;\n const bo = b as Record<string, unknown>;\n const keysA = Object.keys(ao);\n if (keysA.length !== Object.keys(bo).length) {\n return false;\n }\n for (const key of keysA) {\n if (!Object.prototype.hasOwnProperty.call(bo, key)) {\n return false;\n }\n if (!valuesEqual(ao[key], bo[key])) {\n return false;\n }\n }\n return true;\n }\n return false;\n}\n","/**\n * This file is part of helpers4.\n * Copyright (C) 2025 baxyz\n * SPDX-License-Identifier: LGPL-3.0-or-later\n */\n\nimport { ensureDate } from './ensureDate';\nimport type { DateLike } from './types';\n\n/**\n * Checks if a timestamp is likely in seconds (Java/Unix style) vs milliseconds (JavaScript style)\n * @param timestamp - The timestamp to check\n * @returns True if timestamp appears to be in seconds\n * @since 1.9.0\n */\nexport function isTimestampInSeconds(timestamp: number): boolean {\n // Timestamps before year 2001 in milliseconds are less than 10^10\n // Use Math.abs to correctly handle negative timestamps (before epoch)\n return Math.abs(timestamp) < 10000000000;\n}\n\n/**\n * Converts a timestamp to JavaScript milliseconds format\n * @param timestamp - The timestamp (in seconds or milliseconds)\n * @returns Timestamp in milliseconds\n * @since 1.9.0\n */\nexport function normalizeTimestamp(timestamp: number): number {\n return isTimestampInSeconds(timestamp) ? timestamp * 1000 : timestamp;\n}\n\n/**\n * Converts a date to a timestamp in **seconds** (epoch seconds).\n *\n * Use this when sending a date to a backend API that expects seconds\n * (e.g. Java `Instant.getEpochSecond()`, Python `time.time()`).\n *\n * @param date - The date to convert\n * @returns Seconds since the Unix epoch, or `null` for invalid input\n * @example\n * toSeconds('2025-01-19T12:00:00Z') // => 1737288000\n * toSeconds(null) // => null\n *\n * @since 2.0.0\n */\nexport function toSeconds(date: DateLike): number | null {\n const d = ensureDate(date);\n if (!d) return null;\n return Math.floor(d.getTime() / 1000);\n}\n\n/**\n * Converts a date to a timestamp in **milliseconds** (epoch millis).\n *\n * Use this when you need a plain number from a `DateLike` value\n * (e.g. for `Date.now()` comparisons, localStorage, or JS-native APIs).\n *\n * @param date - The date to convert\n * @returns Milliseconds since the Unix epoch, or `null` for invalid input\n * @example\n * toMillis('2025-01-19T12:00:00Z') // => 1737288000000\n * toMillis(null) // => null\n *\n * @since 2.0.0\n */\nexport function toMillis(date: DateLike): number | null {\n const d = ensureDate(date);\n if (!d) return null;\n return d.getTime();\n}\n\n/**\n * Creates a `Date` from a timestamp in **seconds**.\n *\n * Use this when receiving a timestamp from a backend that sends seconds\n * (e.g. Java `Instant.getEpochSecond()`). No heuristic — the input is\n * always treated as seconds.\n *\n * @param seconds - Seconds since the Unix epoch\n * @returns A valid `Date`, or `null` for `NaN` / non-finite input\n * @example\n * fromSeconds(1737288000) // => Date('2025-01-19T12:00:00Z')\n * fromSeconds(0) // => Date('1970-01-01T00:00:00Z')\n * fromSeconds(NaN) // => null\n *\n * @since 2.0.0\n */\nexport function fromSeconds(seconds: number): Date | null {\n if (!Number.isFinite(seconds)) return null;\n return new Date(seconds * 1000);\n}\n\n/**\n * Creates a `Date` from a timestamp in **milliseconds**.\n *\n * Use this when receiving a timestamp from a JS-native source\n * (e.g. `Date.now()`, `performance.timeOrigin`). No heuristic — the\n * input is always treated as milliseconds.\n *\n * @param ms - Milliseconds since the Unix epoch\n * @returns A valid `Date`, or `null` for `NaN` / non-finite input\n * @example\n * fromMillis(1737288000000) // => Date('2025-01-19T12:00:00Z')\n * fromMillis(0) // => Date('1970-01-01T00:00:00Z')\n * fromMillis(NaN) // => null\n *\n * @since 2.0.0\n */\nexport function fromMillis(ms: number): Date | null {\n if (!Number.isFinite(ms)) return null;\n return new Date(ms);\n}\n","/**\n * This file is part of helpers4.\n * Copyright (C) 2025 baxyz\n * SPDX-License-Identifier: LGPL-3.0-or-later\n */\n\nimport { normalizeTimestamp } from './timestamp';\nimport type { DateLike, EpochMilliseconds } from './types';\n\n/**\n * Type guard that checks whether a value satisfies the {@link EpochMilliseconds}\n * structural interface (e.g. `Temporal.Instant`, `Temporal.ZonedDateTime`).\n */\nfunction hasEpochMilliseconds(value: unknown): value is EpochMilliseconds {\n return (\n typeof value === 'object' &&\n value !== null &&\n 'epochMilliseconds' in value &&\n typeof (value as EpochMilliseconds).epochMilliseconds === 'number'\n );\n}\n\n/**\n * Safely converts a date-like value to a valid `Date` object, or returns `null`.\n *\n * Accepts `Date`, timestamps (seconds or milliseconds, auto-detected), date strings,\n * and objects with an `epochMilliseconds` property (e.g. `Temporal.Instant`,\n * `Temporal.ZonedDateTime`).\n * Returns `null` for `null`, `undefined`, empty strings, `0`, and any value that\n * produces an invalid `Date`.\n *\n * This is the date equivalent of {@link ensureArray} — it normalizes flexible\n * input into a guaranteed type (or a safe fallback).\n *\n * @param input - A date-like value to convert\n * @returns A valid `Date` object, or `null` if the input is invalid\n * @example\n * ensureDate('2025-01-19T12:00:00Z') // => Date\n * ensureDate(1737290400) // => Date (from Unix seconds)\n * ensureDate(1737290400000) // => Date (from milliseconds)\n * ensureDate(new Date()) // => Date (same reference)\n * ensureDate(null) // => null\n * ensureDate('invalid') // => null\n *\n * @see {@link DateLike} for the accepted input types\n * @since 2.0.0\n */\nexport function ensureDate(input: DateLike | null | undefined): Date | null {\n if (input === null || input === undefined || input === '' || input === 0) {\n return null;\n }\n\n if (input instanceof Date) {\n return isNaN(input.getTime()) ? null : input;\n }\n\n if (hasEpochMilliseconds(input)) {\n const date = new Date(input.epochMilliseconds);\n return isNaN(date.getTime()) ? null : date;\n }\n\n if (typeof input === 'number') {\n const normalized = normalizeTimestamp(input);\n const date = new Date(normalized);\n return isNaN(date.getTime()) ? null : date;\n }\n\n if (typeof input === 'string') {\n const date = new Date(input);\n return isNaN(date.getTime()) ? null : date;\n }\n\n return null;\n}\n","/**\n * This file is part of helpers4.\n * Copyright (C) 2025 baxyz\n * SPDX-License-Identifier: LGPL-3.0-or-later\n */\n\nimport { ensureDate } from './ensureDate';\nimport type { DateLike } from './types';\n\n/**\n * Options for date comparison\n * @since 2.0.0\n */\nexport interface DateCompareOptions {\n /**\n * Comparison precision\n * - 'milliseconds': Compare all including milliseconds (default)\n * - 'seconds': Ignore milliseconds, compare to the second\n * - 'minutes': Ignore seconds and milliseconds, compare to the minute\n * - 'hours': Ignore minutes, seconds and milliseconds, compare to the hour\n * - 'days': Compare only the date part (ignore time completely)\n * - 'months': Compare only year and month\n * - 'years': Compare only year\n */\n precision?: 'milliseconds' | 'seconds' | 'minutes' | 'hours' | 'days' | 'months' | 'years';\n}\n\n/**\n * Comparison of two dates.\n *\n * Accepts any {@link DateLike} input (Date, timestamp, or date string).\n *\n * @param dateA - First date to compare\n * @param dateB - Second date to compare\n * @param options - Comparison options\n * @returns `true` if dates are identical according to the specified precision, `false` otherwise\n * @example\n * compare('2025-01-19', new Date('2025-01-19T08:00:00Z'), { precision: 'days' })\n * // => true\n * @since 2.0.0\n */\nexport function compare(dateA: DateLike, dateB: DateLike, options: DateCompareOptions = {}): boolean {\n const { precision = 'milliseconds' } = options;\n\n const a = ensureDate(dateA);\n const b = ensureDate(dateB);\n\n // Any invalid → not equal\n if (a === null || b === null) {\n return false;\n }\n\n switch (precision) {\n case 'years':\n return a.getFullYear() === b.getFullYear();\n\n case 'months':\n return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth();\n\n case 'days':\n return a.toDateString() === b.toDateString();\n\n case 'hours': {\n const hoursA = Math.floor(a.getTime() / (1000 * 60 * 60));\n const hoursB = Math.floor(b.getTime() / (1000 * 60 * 60));\n return hoursA === hoursB;\n }\n\n case 'minutes': {\n const minutesA = Math.floor(a.getTime() / (1000 * 60));\n const minutesB = Math.floor(b.getTime() / (1000 * 60));\n return minutesA === minutesB;\n }\n\n case 'seconds': {\n const secondsA = Math.floor(a.getTime() / 1000);\n const secondsB = Math.floor(b.getTime() / 1000);\n return secondsA === secondsB;\n }\n\n case 'milliseconds':\n default:\n return a.getTime() === b.getTime();\n }\n}\n","/**\n * This file is part of helpers4.\n * Copyright (C) 2025 baxyz\n * SPDX-License-Identifier: LGPL-3.0-or-later\n */\n\n/**\n * Determines if a value is a special object that should not have its properties compared deeply.\n * Special objects include: Date, Function, Promise, Observable, RegExp, Error, Map, Set, WeakMap, WeakSet, etc.\n * \n * @param value - The value to check\n * @returns `true` if the value is a special object, `false` otherwise\n * @since 2.0.0\n */\nexport function isSpecialObject(value: unknown): boolean {\n if (value === null || value === undefined) {\n return false;\n }\n\n // Functions are special objects\n if (typeof value === 'function') {\n return true;\n }\n\n // Non-objects are not special\n if (typeof value !== 'object') {\n return false;\n }\n\n // Arrays are not considered special objects for our purposes\n if (Array.isArray(value)) {\n return false;\n }\n\n // Check for built-in special objects\n if (\n value instanceof Date ||\n value instanceof Promise ||\n value instanceof RegExp ||\n value instanceof Error ||\n value instanceof Map ||\n value instanceof Set ||\n value instanceof WeakMap ||\n value instanceof WeakSet ||\n value instanceof ArrayBuffer ||\n value instanceof DataView\n ) {\n return true;\n }\n\n // Check for Observable (RxJS or similar)\n if (value.constructor?.name === 'Observable') {\n return true;\n }\n\n // Check for DOM elements (if in browser environment)\n if (typeof HTMLElement !== 'undefined' && value instanceof HTMLElement) {\n return true;\n }\n\n // Check for other built-in types by constructor name\n const constructorName = value.constructor?.name;\n if (constructorName && [\n 'Buffer', // Node.js Buffer\n 'URL', // Web URL API\n 'URLSearchParams',\n 'File', // Web File API\n 'Blob', // Web Blob API\n 'FormData', // Web FormData API\n 'Headers', // Web Headers API\n 'Request', // Web Request API\n 'Response', // Web Response API\n 'EventTarget', // Web EventTarget API\n 'Symbol' // Symbol objects\n ].includes(constructorName)) {\n return true;\n }\n\n return false;\n}\n","/**\n * This file is part of helpers4.\n * Copyright (C) 2025 baxyz\n * SPDX-License-Identifier: LGPL-3.0-or-later\n */\n\nimport { equalsDeep as arrayDeepEquals } from '../array/equalsDeep';\nimport { compare as compareDate } from '../date/compare';\nimport { isSpecialObject } from '../type/isSpecialObject';\n\n/**\n * Per-key diff result. Each entry tells what differs between the two\n * objects:\n * - `\"onlyA\"` \\u2014 the key exists only on the first input.\n * - `\"onlyB\"` \\u2014 the key exists only on the second input.\n * - `false` \\u2014 both inputs have the key but the values differ.\n * - nested `DiffResult` \\u2014 plain-object values that differ deeper.\n *\n * @since 2.0.0\n */\nexport interface DiffResult {\n [key: string]: 'onlyA' | 'onlyB' | false | DiffResult;\n}\n\n/**\n * Options for {@link diff}.\n * @since 2.0.0\n */\nexport interface DiffOptions {\n /**\n * Maximum recursion depth for nested plain objects. Defaults to\n * `Infinity` (full structural diff).\n *\n * - `0` compares values at the top-level only with strict equality.\n * - `1` compares the first level of nested objects with strict equality.\n * - `Infinity` recurses through every plain-object level.\n *\n * Arrays, `Date` and special objects (Map, Set, RegExp\\u2026) are always\n * leaf-compared (via {@link arrayDeepEquals}, epoch comparison, or\n * reference equality respectively) regardless of `depth`.\n */\n depth?: number;\n}\n\n/**\n * Structural object diff.\n *\n * Returns `true` when both inputs are deeply equal, otherwise a\n * {@link DiffResult} describing the differences key by key.\n *\n * Comparison rules:\n * - Same reference \\u2192 `true`.\n * - Either side is `null`/`undefined` (and not both) \\u2192 `false`.\n * - Both `Date` \\u2192 epoch comparison.\n * - Both arrays \\u2192 compared with `array/equalsDeep` (leaf, no diff drill-down).\n * - Special objects (Map, Set, RegExp, Promise, class instances\\u2026) \\u2192 reference equality.\n * - Plain objects \\u2192 key-by-key, recursing up to `options.depth` levels.\n * - Mixed types (e.g. array vs object, Date vs object) \\u2192 `false`.\n *\n * For a boolean wrapper see {@link equalsDeep} from this category.\n * For a one-level boolean check see {@link equalsShallow} from this category.\n *\n * @param objA - First value (object, `null`, or `undefined`).\n * @param objB - Second value (object, `null`, or `undefined`).\n * @param options - See {@link DiffOptions}.\n * @returns `true` when equal, otherwise a {@link DiffResult}, or `false` for incompatible types.\n * @since 2.0.0\n */\nexport function diff(\n objA: object | undefined | null,\n objB: object | undefined | null,\n options: DiffOptions = {},\n): true | false | DiffResult {\n const depth = options.depth ?? Infinity;\n\n // Quick reference equality check\n if (objA === objB) {\n return true;\n }\n\n // Handle null/undefined cases\n if (objA == null || objB == null) {\n return false;\n }\n\n // Handle Date objects\n if (objA instanceof Date && objB instanceof Date) {\n return compareDate(objA, objB);\n }\n\n // Handle Arrays at root level\n if (Array.isArray(objA) && Array.isArray(objB)) {\n return arrayDeepEquals(objA, objB);\n }\n if (Array.isArray(objA) || Array.isArray(objB)) {\n return false;\n }\n\n // Handle special objects - compare by reference only\n if (isSpecialObject(objA) || isSpecialObject(objB)) {\n return objA === objB;\n }\n\n // Plain object detailed comparison\n return diffPlainObjects(objA, objB, depth);\n}\n\nfunction diffPlainObjects(\n objA: object,\n objB: object,\n depth: number,\n): true | DiffResult {\n const keys = Array.from(new Set([...Object.keys(objA), ...Object.keys(objB)]));\n const differences: DiffResult = Object.create(null) as DiffResult;\n\n for (const key of keys) {\n const hasA = Object.prototype.hasOwnProperty.call(objA, key);\n const hasB = Object.prototype.hasOwnProperty.call(objB, key);\n\n if (!hasA && hasB) {\n differences[key] = 'onlyB';\n continue;\n }\n if (hasA && !hasB) {\n differences[key] = 'onlyA';\n continue;\n }\n\n const valueA = (objA as Record<string, unknown>)[key];\n const valueB = (objB as Record<string, unknown>)[key];\n\n if (valueA === valueB) {\n continue;\n }\n\n if (Array.isArray(valueA) && Array.isArray(valueB)) {\n if (!arrayDeepEquals(valueA, valueB)) {\n differences[key] = false;\n }\n continue;\n }\n if (Array.isArray(valueA) || Array.isArray(valueB)) {\n differences[key] = false;\n continue;\n }\n\n if (valueA instanceof Date && valueB instanceof Date) {\n if (!compareDate(valueA, valueB)) {\n differences[key] = false;\n }\n continue;\n }\n if (valueA instanceof Date || valueB instanceof Date) {\n differences[key] = false;\n continue;\n }\n\n if (isSpecialObject(valueA) || isSpecialObject(valueB)) {\n // valueA === valueB already handled above, so any special-object pair\n // that reaches here must differ.\n differences[key] = false;\n continue;\n }\n\n const bothPlainObjects =\n valueA !== null && valueB !== null &&\n typeof valueA === 'object' && typeof valueB === 'object';\n\n if (!bothPlainObjects) {\n differences[key] = false;\n continue;\n }\n\n if (depth <= 0) {\n // depth budget exhausted \\u2192 strict-equal nested objects only match by reference\n differences[key] = false;\n continue;\n }\n\n const nested = diffPlainObjects(valueA as object, valueB as object, depth - 1);\n if (nested !== true) {\n differences[key] = nested;\n }\n }\n\n return Object.keys(differences).length > 0 ? differences : true;\n}\n","/**\n * This file is part of helpers4.\n * Copyright (C) 2025 baxyz\n * SPDX-License-Identifier: LGPL-3.0-or-later\n */\n\nimport { diff } from './diff';\n\n/**\n * Recursive structural object equality.\n *\n * Boolean wrapper around {@link diff} \\u2014 returns `true` when the two values\n * are deeply equal according to the same rules. Use this when you only\n * need a yes/no answer; use {@link diff} when you also need to know\n * *what* differs.\n *\n * For a one-level boolean check use {@link equalsShallow}.\n *\n * @param objA - First value (object, `null`, or `undefined`).\n * @param objB - Second value (object, `null`, or `undefined`).\n * @returns `true` if both inputs are deeply equal, `false` otherwise.\n * @since 2.0.0\n */\nexport function equalsDeep(\n objA: object | undefined | null,\n objB: object | undefined | null,\n): boolean {\n return diff(objA, objB) === true;\n}\n","/**\n * This file is part of helpers4.\n * Copyright (C) 2025 baxyz\n * SPDX-License-Identifier: LGPL-3.0-or-later\n */\n\n/**\n * One-level (shallow) object equality.\n *\n * Two objects are equal when they share the exact same set of own\n * enumerable string keys and each pair of values satisfies strict equality\n * (`===`). No recursion: nested objects/arrays are compared by reference.\n *\n * Falls back to strict equality when either input is `null`, `undefined`\n * or not an object \\u2014 so primitives match if and only if they are `===`.\n * Arrays are not supported; they always return `false` (unless identical\n * references). Use `array/equalsShallow` instead.\n *\n * For recursive structural comparison use {@link equalsDeep}. For a diff\n * structure use {@link diff}.\n *\n * @param objA - First value to compare\n * @param objB - Second value to compare\n * @returns `true` if values are shallowly equal, `false` otherwise.\n * @since 2.0.0\n */\nexport function equalsShallow(objA: unknown, objB: unknown): boolean {\n if (objA === objB) {\n return true;\n }\n if (\n objA === null || objB === null ||\n typeof objA !== 'object' || typeof objB !== 'object'\n ) {\n return false;\n }\n const a = objA as Record<string, unknown>;\n const b = objB as Record<string, unknown>;\n if (Array.isArray(objA) || Array.isArray(objB)) {\n return false;\n }\n const keysA = Object.keys(a);\n if (keysA.length !== Object.keys(b).length) {\n return false;\n }\n for (const key of keysA) {\n if (!Object.prototype.hasOwnProperty.call(b, key) || a[key] !== b[key]) {\n return false;\n }\n }\n return true;\n}\n","/**\n * This file is part of helpers4.\n * Copyright (C) 2025 baxyz\n * SPDX-License-Identifier: LGPL-3.0-or-later\n */\n\n/**\n * Parses a string path into an array of string/number segments.\n *\n * - Dot separators (`.`) split segments; each segment becomes a string key.\n * - Bracket indices (`[n]`) become number keys.\n * - A leading `.` is treated as \"current level\" and stripped before parsing,\n * so `.[0]` ≡ `[0]` and `.a.b` ≡ `a.b`.\n * - Empty string (or bare `.`) returns `['']` (addresses the `''` key on the root object).\n * - Consecutive dots (`a..b`) produce an empty-string segment: `['a', '', 'b']`.\n * - Non-bracket text trailing a `]` within the same dot-segment throws a RangeError;\n * use a dot separator instead (`'a[0].b'` not `'a[0]b'`).\n *\n * @internal\n * @example\n * parsePath('a.b.c') // => ['a', 'b', 'c'] (all strings)\n * parsePath('layers.1.x') // => ['layers', '1', 'x'] (all strings)\n * parsePath('layers[1].x') // => ['layers', 1, 'x'] (bracket → number)\n * parsePath('.[0]') // => [0] (leading dot stripped)\n * parsePath('a..b') // => ['a', '', 'b'] (empty segment preserved)\n * parsePath('.') // => [''] (leading dot → empty-string key)\n */\n// `g` flag: stateful — callers must reset lastIndex before each use.\nconst BRACKET = /\\[(\\d+)\\]/g;\n\n// Module-level cache: most call sites use a fixed set of string paths (e.g. sort keys),\n// so parsing the same path string repeatedly on every get()/set() call is pure waste.\nconst parsePathCache = new Map<string, readonly (string | number)[]>();\n\nexport function parsePath(path: string): readonly (string | number)[] {\n const cached = parsePathCache.get(path);\n if (cached !== undefined) return cached;\n\n const result = Object.freeze(parsePathUncached(path));\n parsePathCache.set(path, result);\n return result;\n}\n\nfunction parsePathUncached(path: string): (string | number)[] {\n if (path === '') return [''];\n if (path[0] === '.') path = path.slice(1);\n if (path === '') return [''];\n const result: (string | number)[] = [];\n // Split on `.` first so consecutive dots produce empty segments, then parse `[n]` within each.\n for (const dotSegment of path.split('.')) {\n BRACKET.lastIndex = 0;\n let last = 0;\n let match: RegExpExecArray | null;\n while ((match = BRACKET.exec(dotSegment)) !== null) {\n // String prefix before this [n] — omit only when the segment starts with '['.\n if (match.index > last) result.push(dotSegment.slice(last, match.index));\n result.push(parseInt(match[1], 10));\n last = BRACKET.lastIndex;\n }\n if (last === 0) {\n // No brackets found — push the whole dot-segment as a string key.\n result.push(dotSegment);\n } else if (last < dotSegment.length) {\n // Trailing non-bracket text after the last ']' (e.g. 'a[0]b') — ambiguous and\n // almost always a mistake. Throw rather than silently drop 'b'.\n throw new RangeError(\n `parsePath: non-bracket text '${dotSegment.slice(last)}' after ']' in segment '${dotSegment}' — use 'a[0].b' instead of 'a[0]b'`,\n );\n }\n // last === dotSegment.length: segment ended right after the last ']', nothing to push.\n }\n return result;\n}\n","/**\n * This file is part of helpers4.\n * Copyright (C) 2025 baxyz\n * SPDX-License-Identifier: LGPL-3.0-or-later\n */\n\nimport { parsePath } from './_parsePath.js';\nimport type { DeepGet } from './_types.js';\n\n/**\n * Gets a value from an object using a dot/bracket-notated path or explicit key array.\n *\n * **Two path forms are supported:**\n *\n * 1. **String path** — dot notation (`'a.b.c'`) and bracket notation (`'layers[1].name'`)\n * are both accepted and mixed freely. Segments are traversed as string keys; `[n]`\n * indices become numeric keys.\n *\n * 2. **Key array** (`PropertyKey[]`) — explicit array of `string | number | symbol` keys,\n * no parsing performed. Enables symbol-keyed traversal and compile-time type inference:\n * `get(obj, ['a', 'b'] as const)` infers the return type from the path.\n *\n * @param obj - The object to read from\n * @param path - Dot/bracket-notation string or explicit `PropertyKey[]`\n * @param defaultValue - Returned when the path is absent or resolves to `undefined`\n * @returns The value at the path, or `defaultValue`\n * @example\n * get({ a: { b: { c: 42 } } }, 'a.b.c') // => 42\n * get({ a: [1, 2, 3] }, 'a[1]') // => 2\n * get({ a: { b: 0 } }, 'a.b', 99) // => 0 (falsy values are returned as-is)\n * get({ a: 1 }, 'b.c', 'default') // => 'default'\n *\n * const id = Symbol('id')\n * get({ [id]: 'alice' }, [id]) // => 'alice' (symbol key)\n * @since 1.9.0\n */\nexport function get<T = unknown>(obj: unknown, path: string, defaultValue?: T): T | undefined;\nexport function get<T extends object, const Path extends readonly PropertyKey[]>(\n obj: T,\n path: Path,\n defaultValue?: DeepGet<T, Path>,\n): DeepGet<T, Path> | undefined;\nexport function get(obj: unknown, path: string | PropertyKey[], defaultValue?: unknown): unknown {\n const keys: readonly PropertyKey[] = typeof path === 'string' ? parsePath(path) : path;\n let result: unknown = obj;\n\n for (const key of keys) {\n if (result == null || typeof result !== 'object') {\n return defaultValue;\n }\n result = (result as Record<PropertyKey, unknown>)[key];\n }\n\n return result !== undefined ? result : defaultValue;\n}\n","/**\n * This file is part of helpers4.\n * Copyright (C) 2025 baxyz\n * SPDX-License-Identifier: LGPL-3.0-or-later\n */\n\n/**\n * Groups an array of items by a key derived from each item.\n *\n * A thin, typed wrapper around `Object.groupBy` (ES2024) that works on\n * older targets and provides stricter return-type inference.\n *\n * @param items - The array to group\n * @param keyFn - Function that returns the group key for each item\n * @returns A record mapping each key to the array of items with that key\n * @example\n * groupBy([1, 2, 3, 4], n => n % 2 === 0 ? 'even' : 'odd')\n * // => { odd: [1, 3], even: [2, 4] }\n * @since 2.0.0\n */\nexport function groupBy<T, K extends PropertyKey>(\n items: readonly T[],\n keyFn: (item: T) => K\n): Partial<Record<K, T[]>> {\n const result = {} as Partial<Record<K, T[]>>;\n for (const item of items) {\n const key = keyFn(item);\n if (result[key] === undefined) {\n result[key] = [];\n }\n (result[key] as T[]).push(item);\n }\n return result;\n}\n","/**\n * This file is part of helpers4.\n * Copyright (C) 2025 baxyz\n * SPDX-License-Identifier: LGPL-3.0-or-later\n */\n\n/**\n * Returns a new object with keys and values swapped.\n * If multiple keys share the same value, the last one wins.\n *\n * @param obj - The object whose keys and values are to be swapped\n * @returns A new object with values as keys and original keys as values\n * @example\n * invert({ a: 'x', b: 'y', c: 'z' })\n * // => { x: 'a', y: 'b', z: 'c' }\n *\n * invert({ one: 1, two: 2 })\n * // => { 1: 'one', 2: 'two' }\n * @since 2.0.0\n */\nexport function invert<K extends string, V extends PropertyKey>(obj: Record<K, V>): Record<V, K> {\n const result = {} as Record<V, K>;\n for (const key in obj) {\n if (Object.prototype.hasOwnProperty.call(obj, key)) {\n result[obj[key as K]] = key as unknown as K;\n }\n }\n return result;\n}\n","/**\n * This file is part of helpers4.\n * Copyright (C) 2025 baxyz\n * SPDX-License-Identifier: LGPL-3.0-or-later\n */\n\n/**\n * Checks if a plain object has no own enumerable string-keyed properties.\n *\n * Symbol-keyed properties are not counted. Use `Object.getOwnPropertySymbols`\n * separately if symbol keys matter for your use case.\n *\n * @param value - The object to check\n * @returns `true` if the object has no own enumerable string-keyed properties\n * @example\n * isEmpty({}) // => true\n * isEmpty({ a: 1 }) // => false\n * isEmpty({ a: undefined }) // => false (key exists)\n * @since 2.0.3\n */\nexport function isEmpty(value: Record<PropertyKey, unknown>): boolean {\n return Object.keys(value).length === 0;\n}\n","/**\n * This file is part of helpers4.\n * Copyright (C) 2025 baxyz\n * SPDX-License-Identifier: LGPL-3.0-or-later\n */\n\n/**\n * Checks if a plain object has at least one own enumerable string-keyed property.\n *\n * Symbol-keyed properties are not counted. Use `Object.getOwnPropertySymbols`\n * separately if symbol keys matter for your use case.\n *\n * @param value - The object to check\n * @returns `true` if the object has at least one own enumerable string-keyed property\n * @example\n * isNonEmpty({ a: 1 }) // => true\n * isNonEmpty({}) // => false\n * @since 2.0.3\n */\nexport function isNonEmpty(value: Record<PropertyKey, unknown>): boolean {\n return Object.keys(value).length > 0;\n}\n","/**\n * This file is part of helpers4.\n * Copyright (C) 2025 baxyz\n * SPDX-License-Identifier: LGPL-3.0-or-later\n */\n\n/**\n * Transforms the values and/or keys of a plain object in a single pass.\n *\n * Both callbacks are optional and default to identity (no transformation).\n * When `mapValue` is omitted the original values are preserved;\n * when `mapKey` is omitted the original keys are preserved.\n *\n * Note: if two different keys map to the same output key the last one wins\n * (insertion order).\n *\n * @param obj - The source object\n * @param mapValue - Callback called with `(value, key)` for each entry.\n * Defaults to identity.\n * @param mapKey - Callback called with `(key, value)` for each entry.\n * Defaults to identity.\n * @returns A new object with transformed keys and/or values\n * @example\n * map({ a: 1, b: 2 }, v => v * 10)\n * // => { a: 10, b: 20 }\n *\n * map({ a: 1, b: 2 }, undefined, k => k.toUpperCase())\n * // => { A: 1, B: 2 }\n *\n * map({ a: 1, b: 2 }, v => v * 10, k => k.toUpperCase())\n * // => { A: 10, B: 20 }\n * @since 2.0.0\n */\nexport function map<\n TObj extends Record<string, unknown>,\n TVal = TObj[keyof TObj],\n TKey extends PropertyKey = keyof TObj,\n>(\n obj: TObj,\n mapValue?: (value: TObj[keyof TObj], key: keyof TObj) => TVal,\n mapKey?: (key: keyof TObj, value: TObj[keyof TObj]) => TKey\n): Record<TKey, TVal> {\n const result = {} as Record<TKey, TVal>;\n for (const key of Object.keys(obj) as (keyof TObj)[]) {\n const value = obj[key];\n const newKey = mapKey ? mapKey(key, value) : (key as unknown as TKey);\n const newVal = mapValue ? mapValue(value, key) : (value as unknown as TVal);\n result[newKey] = newVal;\n }\n return result;\n}\n","/**\n * This file is part of helpers4.\n * Copyright (C) 2025 baxyz\n * SPDX-License-Identifier: LGPL-3.0-or-later\n */\n\n/**\n * Creates a new object without the specified keys.\n * @param obj - The source object\n * @param keys - The keys to omit\n * @returns A new object without the omitted keys\n * @example\n * omit({ a: 1, b: 2, c: 3 }, ['b'])\n * // => { a: 1, c: 3 }\n * @since 2.0.0\n */\nexport function omit<T extends Record<string, unknown>, K extends keyof T>(obj: T, keys: readonly K[]): Omit<T, K>;\nexport function omit(obj: undefined, keys: readonly string[]): undefined;\nexport function omit(obj: null, keys: readonly string[]): null;\nexport function omit<T extends Record<string, unknown>, K extends keyof T>(\n obj: T | undefined | null,\n keys: readonly K[]\n): Omit<T, K> | undefined | null {\n if (obj === undefined || obj === null) return obj;\n const result = { ...obj };\n for (const key of keys) {\n delete result[key];\n }\n return result as Omit<T, K>;\n}\n","/**\n * This file is part of helpers4.\n * Copyright (C) 2025 baxyz\n * SPDX-License-Identifier: LGPL-3.0-or-later\n */\n\n/**\n * Creates a new object with only the specified keys.\n * @param obj - The source object\n * @param keys - The keys to pick\n * @returns A new object with only the picked keys\n * @example\n * pick({ a: 1, b: 2, c: 3 }, ['a', 'c'])\n * // => { a: 1, c: 3 }\n * @since 2.0.0\n */\nexport function pick<T extends Record<string, unknown>, K extends keyof T>(obj: T, keys: readonly K[]): Pick<T, K>;\nexport function pick(obj: undefined, keys: readonly string[]): undefined;\nexport function pick(obj: null, keys: readonly string[]): null;\nexport function pick<T extends Record<string, unknown>, K extends keyof T>(\n obj: T | undefined | null,\n keys: readonly K[]\n): Pick<T, K> | undefined | null {\n if (obj === undefined || obj === null) return obj;\n const result = {} as Pick<T, K>;\n for (const key of keys) {\n if (Object.prototype.hasOwnProperty.call(obj, key)) {\n result[key] = obj[key];\n }\n }\n return result;\n}\n","/**\n * This file is part of helpers4.\n * Copyright (C) 2025 baxyz\n * SPDX-License-Identifier: LGPL-3.0-or-later\n */\n\n/**\n * Checks if a value is null or undefined (nullish).\n * @param value - The value to check\n * @returns True if value is null or undefined\n * @example\n * isNullish(null) // => true\n * isNullish(undefined) // => true\n * isNullish(0) // => false\n * isNullish('') // => false\n *\n * @see {@link isDefined} for the inverse check (`!isNullish`)\n * @see {@link isFalsy} to also catch `0`, `\"\"`, `false`, and `NaN`\n * @since 2.0.0\n */\nexport function isNullish(value: unknown): value is null | undefined {\n return value === null || value === undefined;\n}\n","/**\n * This file is part of helpers4.\n * Copyright (C) 2025 baxyz\n * SPDX-License-Identifier: LGPL-3.0-or-later\n */\n\nimport { isNullish } from '../type/isNullish';\n\n/**\n * Remove null and undefined values from an object.\n *\n * @param obj an object\n * @returns A shallow copy of the object without null or undefined values\n * @since 1.0.0\n */\nexport function removeUndefinedNull<T extends Record<string, string | number | boolean | null | undefined>>(obj: T): Partial<T>;\n\n/**\n * Remove null and undefined values from an object.\n *\n * @param obj a null object\n * @returns null\n * @since 1.0.0\n */\nexport function removeUndefinedNull(obj: null): null;\n\n/**\n * Remove null and undefined values from an object.\n *\n * @param obj an undefined object\n * @returns undefined\n * @since 1.0.0\n */\nexport function removeUndefinedNull(obj: undefined): undefined;\n\n/**\n * Remove null and undefined values from an object.\n *\n * @param obj an object\n * @returns A shallow copy of the object without null or undefined values\n * @since 1.0.0\n */\nexport function removeUndefinedNull<T extends Record<string, string | number | boolean | null | undefined>>(obj: T | null | undefined): Partial<T> | null | undefined {\n if (!obj) return obj;\n const result: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(obj)) {\n if (!isNullish(v)) { result[k] = v; }\n }\n return result as Partial<T>;\n}\n\n","/**\n * This file is part of helpers4.\n * Copyright (C) 2025 baxyz\n * SPDX-License-Identifier: LGPL-3.0-or-later\n */\n\n/**\n * Parses a JSON string, returning `null` (or a fallback) on any parse failure.\n *\n * Unlike `JSON.parse`, this never throws. Invalid JSON strings and other\n * parsing edge-cases resolve to `null` or the provided `fallback`.\n *\n * @param input - The JSON string to parse.\n * @param fallback - Value returned on failure. Defaults to `null` when omitted.\n * @returns The parsed value typed as `T`, or `fallback` on failure.\n * @example\n * safeJsonParse<{ a: number }>('{\"a\":1}') // => { a: 1 }\n * safeJsonParse('invalid') // => null\n * safeJsonParse('invalid', []) // => []\n * @since 2.0.0\n */\nexport function safeJsonParse<T>(input: string): T | null;\nexport function safeJsonParse<T>(input: string, fallback: T): T;\nexport function safeJsonParse<T>(input: string, fallback?: T): T | null {\n try {\n return JSON.parse(input) as T;\n } catch {\n // Distinguish \"no fallback provided\" (→ null) from \"fallback is undefined\"\n // (→ undefined as T) to keep runtime behaviour aligned with the overloads.\n return arguments.length < 2 ? null : (fallback as T);\n }\n}\n","/**\n * This file is part of helpers4.\n * Copyright (C) 2025 baxyz\n * SPDX-License-Identifier: LGPL-3.0-or-later\n */\n\nimport { parsePath } from './_parsePath.js';\nimport type { DeepGet, DeepSet } from './_types.js';\nimport { UNSAFE_KEYS } from './_unsafeKeys.js';\n\n/**\n * Sets a value in an object at the given path, creating intermediate objects as needed.\n *\n * **Three path forms are supported:**\n *\n * 1. **Dot notation** (`string`) — segments split on `.` are kept as-is string keys.\n * `\"layers.1.name\"` → keys `[\"layers\", \"1\", \"name\"]` (all strings, including `\"1\"`).\n *\n * 2. **Bracket notation** (`string`) — `[n]` segments are parsed as numeric keys.\n * `\"layers[1].name\"` → keys `[\"layers\", 1, \"name\"]` (index `1` becomes a number).\n * Dot and bracket can be mixed freely: `\"a[0].b[2].c\"`.\n *\n * 3. **Key array** (`PropertyKey[]`) — explicit array of `string | number | symbol` keys,\n * no parsing performed. Enables full key-type control, including symbols:\n * `[\"layers\", 1, Symbol('id')]`.\n *\n * Intermediate nodes that are absent, `null`, or not an object are replaced with `{}`.\n * Any path containing a string segment equal to `__proto__`, `constructor`, or `prototype`\n * is rejected and the original object is returned unchanged (prototype-pollution guard).\n *\n * @param obj - The object to mutate\n * @param path - Dot/bracket-notation string or explicit `PropertyKey[]`\n * @param value - Value to assign at the path\n * @returns The mutated object (same reference)\n * @example\n * // 1. Dot notation — \"1\" stays a string key\n * set({}, 'a.b.c', 42)\n * // => { a: { b: { c: 42 } } }\n *\n * set({}, 'layers.1.name', 'bg')\n * // => { layers: { '1': { name: 'bg' } } }\n *\n * // 2. Bracket notation — [1] becomes a number key\n * set({ layers: [{}, { name: 'old' }] }, 'layers[1].name', 'bg')\n * // => { layers: [{}, { name: 'bg' }] }\n *\n * // 3. Key array — supports symbols\n * const id = Symbol('id')\n * set({}, ['user', id], 'alice')\n * // => { user: { [id]: 'alice' } }\n * @since 1.9.0\n */\n\nexport function set(obj: Record<string, unknown>, path: string, value: unknown): Record<string, unknown>;\nexport function set<T extends object, const Path extends readonly PropertyKey[], V extends DeepGet<T, Path>>(obj: T, path: Path, value: V): DeepSet<T, Path, V>;\nexport function set(obj: object, path: string | PropertyKey[], value: unknown): object {\n const keys: readonly PropertyKey[] = typeof path === 'string' ? parsePath(path) : path;\n\n if (keys.length === 0) return obj;\n if (keys.some((k) => typeof k === 'string' && UNSAFE_KEYS.has(k))) return obj;\n\n let current = obj as Record<PropertyKey, unknown>;\n\n for (let i = 0; i < keys.length - 1; i++) {\n const key = keys[i];\n\n // Object.hasOwn (not `key in current`) is intentional: `in` checks the prototype chain,\n // which would silently traverse into — and potentially mutate — an inherited object.\n // Using hasOwn ensures we only follow own properties; inherited intermediate nodes are\n // shadowed with a fresh own `{}` instead of being followed into the prototype.\n if (!Object.hasOwn(current, key) || typeof current[key] !== 'object' || current[key] === null) {\n current[key] = {};\n }\n\n current = current[key] as Record<PropertyKey, unknown>;\n }\n\n current[keys[keys.length - 1]] = value;\n return obj;\n}\n"],"mappings":"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"}
package/llms.txt CHANGED
@@ -1,7 +1,7 @@
1
1
  # @helpers4/object
2
2
 
3
3
  > Tree-shakable TypeScript utility functions for the `object` domain.
4
- > Package: `@helpers4/object` — Version: 2.0.3
4
+ > Package: `@helpers4/object` — Version: 2.0.4
5
5
  > License: LGPL-3.0-or-later
6
6
 
7
7
  ## Installation
@@ -24,11 +24,11 @@ import { compact, deepClone, deepMerge, ... } from '@helpers4/object';
24
24
  |---|---|
25
25
  | `compact` | Removes all entries with falsy values (`false`, `null`, `undefined`, `0`, `""`, `NaN`) from an objec |
26
26
  | `deepClone` | Creates a deep copy of an object or array |
27
- | `deepMerge` | Merges two or more objects deeply |
27
+ | `deepMerge` | Merges two or more objects deeply. Recursively merges own enumerable properties — both string and s |
28
28
  | `diff` | Structural object diff. Returns `true` when both inputs are deeply equal, otherwise a DiffResult de |
29
29
  | `equalsDeep` | Recursive structural object equality. Boolean wrapper around diff \u2014 returns `true` when the tw |
30
30
  | `equalsShallow` | One-level (shallow) object equality. Two objects are equal when they share the exact same set of ow |
31
- | `get` | Gets a value from an object using a dot-notated path |
31
+ | `get` | Gets a value from an object using a dot/bracket-notated path or explicit key array. **Two path form |
32
32
  | `groupBy` | Groups an array of items by a key derived from each item. A thin, typed wrapper around `Object.grou |
33
33
  | `invert` | Returns a new object with keys and values swapped. If multiple keys share the same value, the last o |
34
34
  | `isEmpty` | Checks if a plain object has no own enumerable string-keyed properties. Symbol-keyed properties are |
@@ -38,7 +38,7 @@ import { compact, deepClone, deepMerge, ... } from '@helpers4/object';
38
38
  | `pick` | Creates a new object with only the specified keys. |
39
39
  | `removeUndefinedNull` | Remove null and undefined values from an object. |
40
40
  | `safeJsonParse` | Parses a JSON string, returning `null` (or a fallback) on any parse failure. Unlike `JSON.parse`, t |
41
- | `set` | Sets a value in an object using a dot-notated path |
41
+ | `set` | Sets a value in an object at the given path, creating intermediate objects as needed. **Three path |
42
42
 
43
43
  ---
44
44
 
@@ -139,46 +139,51 @@ cloned.a.b = 2;
139
139
 
140
140
  ### `deepMerge`
141
141
 
142
- Merges two or more objects deeply
142
+ Merges two or more objects deeply.
143
+
144
+ Recursively merges own enumerable properties — both string and symbol keys.
145
+ Plain objects are merged recursively; all other values (arrays, class instances,
146
+ primitives, etc.) are replaced by the source value.
147
+ `undefined` source values do not overwrite existing target values.
143
148
 
144
149
  ```typescript
145
150
  import { deepMerge } from '@helpers4/object';
146
151
 
147
- deepMerge<T extends Record<string, unknown>>(target: T, sources: Record<string, unknown>[]): T
152
+ deepMerge<T extends Record<PropertyKey, unknown>>(target: T, sources: Record<PropertyKey, unknown>[]): T
148
153
  ```
149
154
 
150
155
  **Parameters:**
151
156
 
152
- - `target: T` — The target object
153
- - `sources: Record<string, unknown>[]` — The source objects to merge
157
+ - `target: T` — The target object (mutated in place)
158
+ - `sources: Record<PropertyKey, unknown>[]` — One or more source objects to merge into the target
154
159
 
155
- **Returns:** `T` — The merged object
160
+ **Returns:** `T` — The mutated target
156
161
 
157
162
  ```typescript
158
163
  import { deepMerge } from '@helpers4/object';
159
164
 
160
- deepMerge(target: undefined, sources: Record<string, unknown>[]): undefined
165
+ deepMerge(target: undefined, sources: Record<PropertyKey, unknown>[]): undefined
161
166
  ```
162
167
 
163
168
  **Parameters:**
164
169
 
165
- - `target: undefined` — The target object
166
- - `sources: Record<string, unknown>[]` — The source objects to merge
170
+ - `target: undefined` — The target object (mutated in place)
171
+ - `sources: Record<PropertyKey, unknown>[]` — One or more source objects to merge into the target
167
172
 
168
- **Returns:** `undefined` — The merged object
173
+ **Returns:** `undefined` — The mutated target
169
174
 
170
175
  ```typescript
171
176
  import { deepMerge } from '@helpers4/object';
172
177
 
173
- deepMerge(target: null, sources: Record<string, unknown>[]): null
178
+ deepMerge(target: null, sources: Record<PropertyKey, unknown>[]): null
174
179
  ```
175
180
 
176
181
  **Parameters:**
177
182
 
178
- - `target: null` — The target object
179
- - `sources: Record<string, unknown>[]` — The source objects to merge
183
+ - `target: null` — The target object (mutated in place)
184
+ - `sources: Record<PropertyKey, unknown>[]` — One or more source objects to merge into the target
180
185
 
181
- **Returns:** `null` — The merged object
186
+ **Returns:** `null` — The mutated target
182
187
 
183
188
  **Examples:**
184
189
 
@@ -338,7 +343,17 @@ equalsShallow({ a: 1, b: 2 }, { a: 1, b: 2 })
338
343
 
339
344
  ### `get`
340
345
 
341
- Gets a value from an object using a dot-notated path
346
+ Gets a value from an object using a dot/bracket-notated path or explicit key array.
347
+
348
+ **Two path forms are supported:**
349
+
350
+ 1. **String path** — dot notation (`'a.b.c'`) and bracket notation (`'layers[1].name'`)
351
+ are both accepted and mixed freely. Segments are traversed as string keys; `[n]`
352
+ indices become numeric keys.
353
+
354
+ 2. **Key array** (`PropertyKey[]`) — explicit array of `string | number | symbol` keys,
355
+ no parsing performed. Enables symbol-keyed traversal and compile-time type inference:
356
+ `get(obj, ['a', 'b'] as const)` infers the return type from the path.
342
357
 
343
358
  ```typescript
344
359
  import { get } from '@helpers4/object';
@@ -348,11 +363,25 @@ get<T = unknown>(obj: unknown, path: string, defaultValue?: T): T | undefined
348
363
 
349
364
  **Parameters:**
350
365
 
351
- - `obj: unknown` — The object to get value from
352
- - `path: string` — The dot-notated path (e.g., 'a.b.c')
353
- - `defaultValue?: T` — Default value if path doesn't exist
366
+ - `obj: unknown` — The object to read from
367
+ - `path: string` — Dot/bracket-notation string or explicit `PropertyKey[]`
368
+ - `defaultValue?: T` — Returned when the path is absent or resolves to `undefined`
369
+
370
+ **Returns:** `T | undefined` — The value at the path, or `defaultValue`
371
+
372
+ ```typescript
373
+ import { get } from '@helpers4/object';
374
+
375
+ get<T extends object, Path extends readonly PropertyKey[]>(obj: T, path: Path, defaultValue?: DeepGet<T, Path>): DeepGet<T, Path> | undefined
376
+ ```
377
+
378
+ **Parameters:**
379
+
380
+ - `obj: T` — The object to read from
381
+ - `path: Path` — Dot/bracket-notation string or explicit `PropertyKey[]`
382
+ - `defaultValue?: DeepGet<T, Path>` — Returned when the path is absent or resolves to `undefined`
354
383
 
355
- **Returns:** `T | undefined` — The value at the path or default value
384
+ **Returns:** `DeepGet<T, Path> | undefined` — The value at the path, or `defaultValue`
356
385
 
357
386
  **Examples:**
358
387
 
@@ -374,6 +403,16 @@ get({ a: 1 }, 'b.c', 'default')
374
403
  // => 'default'
375
404
  ```
376
405
 
406
+ *Get via key array (supports symbols)*
407
+
408
+ Pass an explicit PropertyKey[] to bypass parsing. Supports string, number, and symbol keys.
409
+
410
+ ```typescript
411
+ const id = Symbol('id')
412
+ get({ [id]: 'alice' }, [id])
413
+ // => 'alice'
414
+ ```
415
+
377
416
  ---
378
417
 
379
418
  ### `groupBy`
@@ -868,7 +907,24 @@ safeJsonParse('invalid', [])
868
907
 
869
908
  ### `set`
870
909
 
871
- Sets a value in an object using a dot-notated path
910
+ Sets a value in an object at the given path, creating intermediate objects as needed.
911
+
912
+ **Three path forms are supported:**
913
+
914
+ 1. **Dot notation** (`string`) — segments split on `.` are kept as-is string keys.
915
+ `"layers.1.name"` → keys `["layers", "1", "name"]` (all strings, including `"1"`).
916
+
917
+ 2. **Bracket notation** (`string`) — `[n]` segments are parsed as numeric keys.
918
+ `"layers[1].name"` → keys `["layers", 1, "name"]` (index `1` becomes a number).
919
+ Dot and bracket can be mixed freely: `"a[0].b[2].c"`.
920
+
921
+ 3. **Key array** (`PropertyKey[]`) — explicit array of `string | number | symbol` keys,
922
+ no parsing performed. Enables full key-type control, including symbols:
923
+ `["layers", 1, Symbol('id')]`.
924
+
925
+ Intermediate nodes that are absent, `null`, or not an object are replaced with `{}`.
926
+ Any path containing a string segment equal to `__proto__`, `constructor`, or `prototype`
927
+ is rejected and the original object is returned unchanged (prototype-pollution guard).
872
928
 
873
929
  ```typescript
874
930
  import { set } from '@helpers4/object';
@@ -878,21 +934,58 @@ set(obj: Record<string, unknown>, path: string, value: unknown): Record<string,
878
934
 
879
935
  **Parameters:**
880
936
 
881
- - `obj: Record<string, unknown>` — The object to set value in
882
- - `path: string` — The dot-notated path (e.g., 'a.b.c')
883
- - `value: unknown` — The value to set
937
+ - `obj: Record<string, unknown>` — The object to mutate
938
+ - `path: string` — Dot/bracket-notation string or explicit `PropertyKey[]`
939
+ - `value: unknown` — Value to assign at the path
940
+
941
+ **Returns:** `Record<string, unknown>` — The mutated object (same reference)
942
+
943
+ ```typescript
944
+ import { set } from '@helpers4/object';
884
945
 
885
- **Returns:** `Record<string, unknown>` — The modified object
946
+ set<T extends object, Path extends readonly PropertyKey[], V extends unknown>(obj: T, path: Path, value: V): DeepSet<T, Path, V>
947
+ ```
948
+
949
+ **Parameters:**
950
+
951
+ - `obj: T` — The object to mutate
952
+ - `path: Path` — Dot/bracket-notation string or explicit `PropertyKey[]`
953
+ - `value: V` — Value to assign at the path
954
+
955
+ **Returns:** `DeepSet<T, Path, V>` — The mutated object (same reference)
886
956
 
887
957
  **Examples:**
888
958
 
889
- *Set a nested property*
959
+ *Set a nested property (dot notation)*
890
960
 
891
- Creates intermediate objects as needed along the dot-notated path.
961
+ Creates intermediate objects as needed. All segments are string keys — including numeric-looking ones like "1".
892
962
 
893
963
  ```typescript
894
964
  set({}, 'a.b.c', 42)
895
965
  // => { a: { b: { c: 42 } } }
966
+
967
+ set({}, 'layers.1.name', 'bg')
968
+ // => { layers: { '1': { name: 'bg' } } } // '1' is a string key
969
+ ```
970
+
971
+ *Set via bracket notation*
972
+
973
+ Square-bracket indices become numeric keys. Useful when the path targets an array element.
974
+
975
+ ```typescript
976
+ const obj = { layers: [{}, { name: 'old' }] }
977
+ set(obj, 'layers[1].name', 'new')
978
+ // => { layers: [{}, { name: 'new' }] }
979
+ ```
980
+
981
+ *Set via key array (supports symbols)*
982
+
983
+ Pass an explicit PropertyKey[] to bypass parsing. Supports string, number, and symbol keys.
984
+
985
+ ```typescript
986
+ const id = Symbol('id')
987
+ set({}, ['user', id], 'alice')
988
+ // => { user: { [id]: 'alice' } }
896
989
  ```
897
990
 
898
991
  ---
package/meta/api.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "category": "object",
3
- "version": "2.0.3",
3
+ "version": "2.0.4",
4
4
  "functions": [
5
5
  {
6
6
  "name": "compact",
@@ -113,73 +113,73 @@
113
113
  {
114
114
  "name": "deepMerge",
115
115
  "kind": "function",
116
- "description": "Merges two or more objects deeply",
116
+ "description": "Merges two or more objects deeply.\n\nRecursively merges own enumerable properties — both string and symbol keys.\nPlain objects are merged recursively; all other values (arrays, class instances,\nprimitives, etc.) are replaced by the source value.\n`undefined` source values do not overwrite existing target values.",
117
117
  "since": "1.9.0",
118
118
  "signatures": [
119
119
  {
120
- "signature": "deepMerge<T extends Record<string, unknown>>(target: T, sources: Record<string, unknown>[]): T",
121
- "description": "Merges two or more objects deeply",
120
+ "signature": "deepMerge<T extends Record<PropertyKey, unknown>>(target: T, sources: Record<PropertyKey, unknown>[]): T",
121
+ "description": "Merges two or more objects deeply.\n\nRecursively merges own enumerable properties — both string and symbol keys.\nPlain objects are merged recursively; all other values (arrays, class instances,\nprimitives, etc.) are replaced by the source value.\n`undefined` source values do not overwrite existing target values.",
122
122
  "params": [
123
123
  {
124
124
  "name": "target",
125
125
  "type": "T",
126
- "description": "The target object"
126
+ "description": "The target object (mutated in place)"
127
127
  },
128
128
  {
129
129
  "name": "sources",
130
- "type": "Record<string, unknown>[]",
131
- "description": "The source objects to merge"
130
+ "type": "Record<PropertyKey, unknown>[]",
131
+ "description": "One or more source objects to merge into the target"
132
132
  }
133
133
  ],
134
134
  "returns": {
135
135
  "type": "T",
136
- "description": "The merged object"
136
+ "description": "The mutated target"
137
137
  },
138
138
  "typeParameters": [
139
139
  {
140
140
  "name": "T",
141
- "constraint": "Record<string, unknown>"
141
+ "constraint": "Record<PropertyKey, unknown>"
142
142
  }
143
143
  ]
144
144
  },
145
145
  {
146
- "signature": "deepMerge(target: undefined, sources: Record<string, unknown>[]): undefined",
147
- "description": "Merges two or more objects deeply",
146
+ "signature": "deepMerge(target: undefined, sources: Record<PropertyKey, unknown>[]): undefined",
147
+ "description": "Merges two or more objects deeply.\n\nRecursively merges own enumerable properties — both string and symbol keys.\nPlain objects are merged recursively; all other values (arrays, class instances,\nprimitives, etc.) are replaced by the source value.\n`undefined` source values do not overwrite existing target values.",
148
148
  "params": [
149
149
  {
150
150
  "name": "target",
151
151
  "type": "undefined",
152
- "description": "The target object"
152
+ "description": "The target object (mutated in place)"
153
153
  },
154
154
  {
155
155
  "name": "sources",
156
- "type": "Record<string, unknown>[]",
157
- "description": "The source objects to merge"
156
+ "type": "Record<PropertyKey, unknown>[]",
157
+ "description": "One or more source objects to merge into the target"
158
158
  }
159
159
  ],
160
160
  "returns": {
161
161
  "type": "undefined",
162
- "description": "The merged object"
162
+ "description": "The mutated target"
163
163
  }
164
164
  },
165
165
  {
166
- "signature": "deepMerge(target: null, sources: Record<string, unknown>[]): null",
167
- "description": "Merges two or more objects deeply",
166
+ "signature": "deepMerge(target: null, sources: Record<PropertyKey, unknown>[]): null",
167
+ "description": "Merges two or more objects deeply.\n\nRecursively merges own enumerable properties — both string and symbol keys.\nPlain objects are merged recursively; all other values (arrays, class instances,\nprimitives, etc.) are replaced by the source value.\n`undefined` source values do not overwrite existing target values.",
168
168
  "params": [
169
169
  {
170
170
  "name": "target",
171
171
  "type": "null",
172
- "description": "The target object"
172
+ "description": "The target object (mutated in place)"
173
173
  },
174
174
  {
175
175
  "name": "sources",
176
- "type": "Record<string, unknown>[]",
177
- "description": "The source objects to merge"
176
+ "type": "Record<PropertyKey, unknown>[]",
177
+ "description": "One or more source objects to merge into the target"
178
178
  }
179
179
  ],
180
180
  "returns": {
181
181
  "type": "null",
182
- "description": "The merged object"
182
+ "description": "The mutated target"
183
183
  }
184
184
  }
185
185
  ],
@@ -331,33 +331,33 @@
331
331
  {
332
332
  "name": "get",
333
333
  "kind": "function",
334
- "description": "Gets a value from an object using a dot-notated path",
334
+ "description": "Gets a value from an object using a dot/bracket-notated path or explicit key array.\n\n**Two path forms are supported:**\n\n1. **String path** — dot notation (`'a.b.c'`) and bracket notation (`'layers[1].name'`)\n are both accepted and mixed freely. Segments are traversed as string keys; `[n]`\n indices become numeric keys.\n\n2. **Key array** (`PropertyKey[]`) — explicit array of `string | number | symbol` keys,\n no parsing performed. Enables symbol-keyed traversal and compile-time type inference:\n `get(obj, ['a', 'b'] as const)` infers the return type from the path.",
335
335
  "since": "1.9.0",
336
336
  "signatures": [
337
337
  {
338
338
  "signature": "get<T = unknown>(obj: unknown, path: string, defaultValue?: T): T | undefined",
339
- "description": "Gets a value from an object using a dot-notated path",
339
+ "description": "Gets a value from an object using a dot/bracket-notated path or explicit key array.\n\n**Two path forms are supported:**\n\n1. **String path** — dot notation (`'a.b.c'`) and bracket notation (`'layers[1].name'`)\n are both accepted and mixed freely. Segments are traversed as string keys; `[n]`\n indices become numeric keys.\n\n2. **Key array** (`PropertyKey[]`) — explicit array of `string | number | symbol` keys,\n no parsing performed. Enables symbol-keyed traversal and compile-time type inference:\n `get(obj, ['a', 'b'] as const)` infers the return type from the path.",
340
340
  "params": [
341
341
  {
342
342
  "name": "obj",
343
343
  "type": "unknown",
344
- "description": "The object to get value from"
344
+ "description": "The object to read from"
345
345
  },
346
346
  {
347
347
  "name": "path",
348
348
  "type": "string",
349
- "description": "The dot-notated path (e.g., 'a.b.c')"
349
+ "description": "Dot/bracket-notation string or explicit `PropertyKey[]`"
350
350
  },
351
351
  {
352
352
  "name": "defaultValue",
353
353
  "type": "T",
354
- "description": "Default value if path doesn't exist",
354
+ "description": "Returned when the path is absent or resolves to `undefined`",
355
355
  "optional": true
356
356
  }
357
357
  ],
358
358
  "returns": {
359
359
  "type": "T | undefined",
360
- "description": "The value at the path or default value"
360
+ "description": "The value at the path, or `defaultValue`"
361
361
  },
362
362
  "typeParameters": [
363
363
  {
@@ -365,6 +365,42 @@
365
365
  "default": "unknown"
366
366
  }
367
367
  ]
368
+ },
369
+ {
370
+ "signature": "get<T extends object, Path extends readonly PropertyKey[]>(obj: T, path: Path, defaultValue?: DeepGet<T, Path>): DeepGet<T, Path> | undefined",
371
+ "description": "Gets a value from an object using a dot/bracket-notated path or explicit key array.\n\n**Two path forms are supported:**\n\n1. **String path** — dot notation (`'a.b.c'`) and bracket notation (`'layers[1].name'`)\n are both accepted and mixed freely. Segments are traversed as string keys; `[n]`\n indices become numeric keys.\n\n2. **Key array** (`PropertyKey[]`) — explicit array of `string | number | symbol` keys,\n no parsing performed. Enables symbol-keyed traversal and compile-time type inference:\n `get(obj, ['a', 'b'] as const)` infers the return type from the path.",
372
+ "params": [
373
+ {
374
+ "name": "obj",
375
+ "type": "T",
376
+ "description": "The object to read from"
377
+ },
378
+ {
379
+ "name": "path",
380
+ "type": "Path",
381
+ "description": "Dot/bracket-notation string or explicit `PropertyKey[]`"
382
+ },
383
+ {
384
+ "name": "defaultValue",
385
+ "type": "DeepGet<T, Path>",
386
+ "description": "Returned when the path is absent or resolves to `undefined`",
387
+ "optional": true
388
+ }
389
+ ],
390
+ "returns": {
391
+ "type": "DeepGet<T, Path> | undefined",
392
+ "description": "The value at the path, or `defaultValue`"
393
+ },
394
+ "typeParameters": [
395
+ {
396
+ "name": "T",
397
+ "constraint": "object"
398
+ },
399
+ {
400
+ "name": "Path",
401
+ "constraint": "readonly PropertyKey[]"
402
+ }
403
+ ]
368
404
  }
369
405
  ],
370
406
  "examples": [
@@ -377,6 +413,11 @@
377
413
  "title": "Return default for missing path",
378
414
  "description": "Returns the default value when the path does not exist.",
379
415
  "code": "get({ a: 1 }, 'b.c', 'default')\n// => 'default'"
416
+ },
417
+ {
418
+ "title": "Get via key array (supports symbols)",
419
+ "description": "Pass an explicit PropertyKey[] to bypass parsing. Supports string, number, and symbol keys.",
420
+ "code": "const id = Symbol('id')\nget({ [id]: 'alice' }, [id])\n// => 'alice'"
380
421
  }
381
422
  ],
382
423
  "sourceFile": "get.ts"
@@ -940,40 +981,89 @@
940
981
  {
941
982
  "name": "set",
942
983
  "kind": "function",
943
- "description": "Sets a value in an object using a dot-notated path",
984
+ "description": "Sets a value in an object at the given path, creating intermediate objects as needed.\n\n**Three path forms are supported:**\n\n1. **Dot notation** (`string`) — segments split on `.` are kept as-is string keys.\n `\"layers.1.name\"` → keys `[\"layers\", \"1\", \"name\"]` (all strings, including `\"1\"`).\n\n2. **Bracket notation** (`string`) — `[n]` segments are parsed as numeric keys.\n `\"layers[1].name\"` → keys `[\"layers\", 1, \"name\"]` (index `1` becomes a number).\n Dot and bracket can be mixed freely: `\"a[0].b[2].c\"`.\n\n3. **Key array** (`PropertyKey[]`) — explicit array of `string | number | symbol` keys,\n no parsing performed. Enables full key-type control, including symbols:\n `[\"layers\", 1, Symbol('id')]`.\n\nIntermediate nodes that are absent, `null`, or not an object are replaced with `{}`.\nAny path containing a string segment equal to `__proto__`, `constructor`, or `prototype`\nis rejected and the original object is returned unchanged (prototype-pollution guard).",
944
985
  "since": "1.9.0",
945
986
  "signatures": [
946
987
  {
947
988
  "signature": "set(obj: Record<string, unknown>, path: string, value: unknown): Record<string, unknown>",
948
- "description": "Sets a value in an object using a dot-notated path",
989
+ "description": "Sets a value in an object at the given path, creating intermediate objects as needed.\n\n**Three path forms are supported:**\n\n1. **Dot notation** (`string`) — segments split on `.` are kept as-is string keys.\n `\"layers.1.name\"` → keys `[\"layers\", \"1\", \"name\"]` (all strings, including `\"1\"`).\n\n2. **Bracket notation** (`string`) — `[n]` segments are parsed as numeric keys.\n `\"layers[1].name\"` → keys `[\"layers\", 1, \"name\"]` (index `1` becomes a number).\n Dot and bracket can be mixed freely: `\"a[0].b[2].c\"`.\n\n3. **Key array** (`PropertyKey[]`) — explicit array of `string | number | symbol` keys,\n no parsing performed. Enables full key-type control, including symbols:\n `[\"layers\", 1, Symbol('id')]`.\n\nIntermediate nodes that are absent, `null`, or not an object are replaced with `{}`.\nAny path containing a string segment equal to `__proto__`, `constructor`, or `prototype`\nis rejected and the original object is returned unchanged (prototype-pollution guard).",
949
990
  "params": [
950
991
  {
951
992
  "name": "obj",
952
993
  "type": "Record<string, unknown>",
953
- "description": "The object to set value in"
994
+ "description": "The object to mutate"
954
995
  },
955
996
  {
956
997
  "name": "path",
957
998
  "type": "string",
958
- "description": "The dot-notated path (e.g., 'a.b.c')"
999
+ "description": "Dot/bracket-notation string or explicit `PropertyKey[]`"
959
1000
  },
960
1001
  {
961
1002
  "name": "value",
962
1003
  "type": "unknown",
963
- "description": "The value to set"
1004
+ "description": "Value to assign at the path"
964
1005
  }
965
1006
  ],
966
1007
  "returns": {
967
1008
  "type": "Record<string, unknown>",
968
- "description": "The modified object"
1009
+ "description": "The mutated object (same reference)"
969
1010
  }
1011
+ },
1012
+ {
1013
+ "signature": "set<T extends object, Path extends readonly PropertyKey[], V extends unknown>(obj: T, path: Path, value: V): DeepSet<T, Path, V>",
1014
+ "description": "Sets a value in an object at the given path, creating intermediate objects as needed.\n\n**Three path forms are supported:**\n\n1. **Dot notation** (`string`) — segments split on `.` are kept as-is string keys.\n `\"layers.1.name\"` → keys `[\"layers\", \"1\", \"name\"]` (all strings, including `\"1\"`).\n\n2. **Bracket notation** (`string`) — `[n]` segments are parsed as numeric keys.\n `\"layers[1].name\"` → keys `[\"layers\", 1, \"name\"]` (index `1` becomes a number).\n Dot and bracket can be mixed freely: `\"a[0].b[2].c\"`.\n\n3. **Key array** (`PropertyKey[]`) — explicit array of `string | number | symbol` keys,\n no parsing performed. Enables full key-type control, including symbols:\n `[\"layers\", 1, Symbol('id')]`.\n\nIntermediate nodes that are absent, `null`, or not an object are replaced with `{}`.\nAny path containing a string segment equal to `__proto__`, `constructor`, or `prototype`\nis rejected and the original object is returned unchanged (prototype-pollution guard).",
1015
+ "params": [
1016
+ {
1017
+ "name": "obj",
1018
+ "type": "T",
1019
+ "description": "The object to mutate"
1020
+ },
1021
+ {
1022
+ "name": "path",
1023
+ "type": "Path",
1024
+ "description": "Dot/bracket-notation string or explicit `PropertyKey[]`"
1025
+ },
1026
+ {
1027
+ "name": "value",
1028
+ "type": "V",
1029
+ "description": "Value to assign at the path"
1030
+ }
1031
+ ],
1032
+ "returns": {
1033
+ "type": "DeepSet<T, Path, V>",
1034
+ "description": "The mutated object (same reference)"
1035
+ },
1036
+ "typeParameters": [
1037
+ {
1038
+ "name": "T",
1039
+ "constraint": "object"
1040
+ },
1041
+ {
1042
+ "name": "Path",
1043
+ "constraint": "readonly PropertyKey[]"
1044
+ },
1045
+ {
1046
+ "name": "V",
1047
+ "constraint": "unknown"
1048
+ }
1049
+ ]
970
1050
  }
971
1051
  ],
972
1052
  "examples": [
973
1053
  {
974
- "title": "Set a nested property",
975
- "description": "Creates intermediate objects as needed along the dot-notated path.",
976
- "code": "set({}, 'a.b.c', 42)\n// => { a: { b: { c: 42 } } }"
1054
+ "title": "Set a nested property (dot notation)",
1055
+ "description": "Creates intermediate objects as needed. All segments are string keys — including numeric-looking ones like \"1\".",
1056
+ "code": "set({}, 'a.b.c', 42)\n// => { a: { b: { c: 42 } } }\n\nset({}, 'layers.1.name', 'bg')\n// => { layers: { '1': { name: 'bg' } } } // '1' is a string key"
1057
+ },
1058
+ {
1059
+ "title": "Set via bracket notation",
1060
+ "description": "Square-bracket indices become numeric keys. Useful when the path targets an array element.",
1061
+ "code": "const obj = { layers: [{}, { name: 'old' }] }\nset(obj, 'layers[1].name', 'new')\n// => { layers: [{}, { name: 'new' }] }"
1062
+ },
1063
+ {
1064
+ "title": "Set via key array (supports symbols)",
1065
+ "description": "Pass an explicit PropertyKey[] to bypass parsing. Supports string, number, and symbol keys.",
1066
+ "code": "const id = Symbol('id')\nset({}, ['user', id], 'alice')\n// => { user: { [id]: 'alice' } }"
977
1067
  }
978
1068
  ],
979
1069
  "sourceFile": "set.ts"
@@ -88,6 +88,11 @@
88
88
  "title": "Return default for missing path",
89
89
  "description": "Returns the default value when the path does not exist.",
90
90
  "code": "get({ a: 1 }, 'b.c', 'default')\n// => 'default'"
91
+ },
92
+ {
93
+ "title": "Get via key array (supports symbols)",
94
+ "description": "Pass an explicit PropertyKey[] to bypass parsing. Supports string, number, and symbol keys.",
95
+ "code": "const id = Symbol('id')\nget({ [id]: 'alice' }, [id])\n// => 'alice'"
91
96
  }
92
97
  ]
93
98
  },
@@ -235,9 +240,19 @@
235
240
  "name": "set",
236
241
  "examples": [
237
242
  {
238
- "title": "Set a nested property",
239
- "description": "Creates intermediate objects as needed along the dot-notated path.",
240
- "code": "set({}, 'a.b.c', 42)\n// => { a: { b: { c: 42 } } }"
243
+ "title": "Set a nested property (dot notation)",
244
+ "description": "Creates intermediate objects as needed. All segments are string keys — including numeric-looking ones like \"1\".",
245
+ "code": "set({}, 'a.b.c', 42)\n// => { a: { b: { c: 42 } } }\n\nset({}, 'layers.1.name', 'bg')\n// => { layers: { '1': { name: 'bg' } } } // '1' is a string key"
246
+ },
247
+ {
248
+ "title": "Set via bracket notation",
249
+ "description": "Square-bracket indices become numeric keys. Useful when the path targets an array element.",
250
+ "code": "const obj = { layers: [{}, { name: 'old' }] }\nset(obj, 'layers[1].name', 'new')\n// => { layers: [{}, { name: 'new' }] }"
251
+ },
252
+ {
253
+ "title": "Set via key array (supports symbols)",
254
+ "description": "Pass an explicit PropertyKey[] to bypass parsing. Supports string, number, and symbol keys.",
255
+ "code": "const id = Symbol('id')\nset({}, ['user', id], 'alice')\n// => { user: { [id]: 'alice' } }"
241
256
  }
242
257
  ]
243
258
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@helpers4/object",
3
- "version": "2.0.3",
3
+ "version": "2.0.4",
4
4
  "description": "A set of helpers in TS/JS, compatible with tree-shaking, for object.",
5
5
  "author": "baxyz <baxy@etik.com>",
6
6
  "license": "LGPL-3.0",