@helpers4/object 2.0.2 → 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/README.md CHANGED
@@ -21,6 +21,8 @@ A set of helpers for working with URLs.
21
21
  - get
22
22
  - groupBy
23
23
  - invert
24
+ - isEmpty
25
+ - isNonEmpty
24
26
  - map
25
27
  - omit
26
28
  - pick
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.
@@ -214,6 +271,47 @@ declare function groupBy<T, K extends PropertyKey>(items: readonly T[], keyFn: (
214
271
  */
215
272
  declare function invert<K extends string, V extends PropertyKey>(obj: Record<K, V>): Record<V, K>;
216
273
 
274
+ /**
275
+ * This file is part of helpers4.
276
+ * Copyright (C) 2025 baxyz
277
+ * SPDX-License-Identifier: LGPL-3.0-or-later
278
+ */
279
+ /**
280
+ * Checks if a plain object has no own enumerable string-keyed properties.
281
+ *
282
+ * Symbol-keyed properties are not counted. Use `Object.getOwnPropertySymbols`
283
+ * separately if symbol keys matter for your use case.
284
+ *
285
+ * @param value - The object to check
286
+ * @returns `true` if the object has no own enumerable string-keyed properties
287
+ * @example
288
+ * isEmpty({}) // => true
289
+ * isEmpty({ a: 1 }) // => false
290
+ * isEmpty({ a: undefined }) // => false (key exists)
291
+ * @since 2.0.3
292
+ */
293
+ declare function isEmpty(value: Record<PropertyKey, unknown>): boolean;
294
+
295
+ /**
296
+ * This file is part of helpers4.
297
+ * Copyright (C) 2025 baxyz
298
+ * SPDX-License-Identifier: LGPL-3.0-or-later
299
+ */
300
+ /**
301
+ * Checks if a plain object has at least one own enumerable string-keyed property.
302
+ *
303
+ * Symbol-keyed properties are not counted. Use `Object.getOwnPropertySymbols`
304
+ * separately if symbol keys matter for your use case.
305
+ *
306
+ * @param value - The object to check
307
+ * @returns `true` if the object has at least one own enumerable string-keyed property
308
+ * @example
309
+ * isNonEmpty({ a: 1 }) // => true
310
+ * isNonEmpty({}) // => false
311
+ * @since 2.0.3
312
+ */
313
+ declare function isNonEmpty(value: Record<PropertyKey, unknown>): boolean;
314
+
217
315
  /**
218
316
  * This file is part of helpers4.
219
317
  * Copyright (C) 2025 baxyz
@@ -344,15 +442,51 @@ declare function safeJsonParse<T>(input: string, fallback: T): T;
344
442
  * Copyright (C) 2025 baxyz
345
443
  * SPDX-License-Identifier: LGPL-3.0-or-later
346
444
  */
445
+
347
446
  /**
348
- * Sets a value in an object using a dot-notated path
349
- * @param obj - The object to set value in
350
- * @param path - The dot-notated path (e.g., 'a.b.c')
351
- * @param value - The value to set
352
- * @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' } }
353
486
  * @since 1.9.0
354
487
  */
355
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>;
356
490
 
357
- export { compact, deepClone, deepMerge, diff, equalsDeep, equalsShallow, get, groupBy, invert, map, omit, pick, removeUndefinedNull, safeJsonParse, set };
491
+ export { compact, deepClone, deepMerge, diff, equalsDeep, equalsShallow, get, groupBy, invert, isEmpty, isNonEmpty, map, omit, pick, removeUndefinedNull, safeJsonParse, set };
358
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;
@@ -523,6 +586,53 @@ function invert(obj) {
523
586
  return result;
524
587
  }
525
588
  //#endregion
589
+ //#region helpers/object/isEmpty.ts
590
+ /**
591
+ * This file is part of helpers4.
592
+ * Copyright (C) 2025 baxyz
593
+ * SPDX-License-Identifier: LGPL-3.0-or-later
594
+ */
595
+ /**
596
+ * Checks if a plain object has no own enumerable string-keyed properties.
597
+ *
598
+ * Symbol-keyed properties are not counted. Use `Object.getOwnPropertySymbols`
599
+ * separately if symbol keys matter for your use case.
600
+ *
601
+ * @param value - The object to check
602
+ * @returns `true` if the object has no own enumerable string-keyed properties
603
+ * @example
604
+ * isEmpty({}) // => true
605
+ * isEmpty({ a: 1 }) // => false
606
+ * isEmpty({ a: undefined }) // => false (key exists)
607
+ * @since 2.0.3
608
+ */
609
+ function isEmpty(value) {
610
+ return Object.keys(value).length === 0;
611
+ }
612
+ //#endregion
613
+ //#region helpers/object/isNonEmpty.ts
614
+ /**
615
+ * This file is part of helpers4.
616
+ * Copyright (C) 2025 baxyz
617
+ * SPDX-License-Identifier: LGPL-3.0-or-later
618
+ */
619
+ /**
620
+ * Checks if a plain object has at least one own enumerable string-keyed property.
621
+ *
622
+ * Symbol-keyed properties are not counted. Use `Object.getOwnPropertySymbols`
623
+ * separately if symbol keys matter for your use case.
624
+ *
625
+ * @param value - The object to check
626
+ * @returns `true` if the object has at least one own enumerable string-keyed property
627
+ * @example
628
+ * isNonEmpty({ a: 1 }) // => true
629
+ * isNonEmpty({}) // => false
630
+ * @since 2.0.3
631
+ */
632
+ function isNonEmpty(value) {
633
+ return Object.keys(value).length > 0;
634
+ }
635
+ //#endregion
526
636
  //#region helpers/object/map.ts
527
637
  /**
528
638
  * This file is part of helpers4.
@@ -641,32 +751,20 @@ function safeJsonParse(input, fallback) {
641
751
  * Copyright (C) 2025 baxyz
642
752
  * SPDX-License-Identifier: LGPL-3.0-or-later
643
753
  */
644
- var UNSAFE_KEYS = new Set([
645
- "__proto__",
646
- "constructor",
647
- "prototype"
648
- ]);
649
- /**
650
- * Sets a value in an object using a dot-notated path
651
- * @param obj - The object to set value in
652
- * @param path - The dot-notated path (e.g., 'a.b.c')
653
- * @param value - The value to set
654
- * @returns The modified object
655
- * @since 1.9.0
656
- */
657
754
  function set(obj, path, value) {
658
- const keys = path.split(".");
659
- 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;
660
758
  let current = obj;
661
759
  for (let i = 0; i < keys.length - 1; i++) {
662
760
  const key = keys[i];
663
- 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] = {};
664
762
  current = current[key];
665
763
  }
666
764
  current[keys[keys.length - 1]] = value;
667
765
  return obj;
668
766
  }
669
767
  //#endregion
670
- export { compact, deepClone, deepMerge, diff, equalsDeep, equalsShallow, get, groupBy, invert, map, omit, pick, removeUndefinedNull, safeJsonParse, set };
768
+ export { compact, deepClone, deepMerge, diff, equalsDeep, equalsShallow, get, groupBy, invert, isEmpty, isNonEmpty, map, omit, pick, removeUndefinedNull, safeJsonParse, set };
671
769
 
672
770
  //# sourceMappingURL=index.js.map