@helpers4/object 2.0.4 → 3.0.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -12,9 +12,8 @@ A set of helpers for working with URLs.
12
12
  [https://helpers4.dev/typescript/categories/object/](https://helpers4.dev/typescript/categories/object/)
13
13
 
14
14
  <!-- AUTOMATIC-METHODS -->
15
+ - cloneDeep
15
16
  - compact
16
- - deepClone
17
- - deepMerge
18
17
  - diff
19
18
  - equalsDeep
20
19
  - equalsShallow
@@ -24,6 +23,7 @@ A set of helpers for working with URLs.
24
23
  - isEmpty
25
24
  - isNonEmpty
26
25
  - map
26
+ - mergeDeep
27
27
  - omit
28
28
  - pick
29
29
  - removeUndefinedNull
@@ -39,6 +39,7 @@ A set of helpers for working with URLs.
39
39
  - [commit](../commit)
40
40
  - [date](../date)
41
41
  - [function](../function)
42
+ - [guard](../guard)
42
43
  - [id](../id)
43
44
  - [markdown](../markdown)
44
45
  - [node](../node)
@@ -61,6 +62,7 @@ A set of helpers for working with URLs.
61
62
  | commit | [@helpers4/commit](https://www.npmjs.com/package/@helpers4/commit) | [Source](https://github.com/helpers4/helpers4/tree/main/helpers/commit) | Conventional Commits parsing and analysis |
62
63
  | date | [@helpers4/date](https://www.npmjs.com/package/@helpers4/date) | [Source](https://github.com/helpers4/helpers4/tree/main/helpers/date) | Date and time utility functions |
63
64
  | function | [@helpers4/function](https://www.npmjs.com/package/@helpers4/function) | [Source](https://github.com/helpers4/helpers4/tree/main/helpers/function) | Function utilities and type guards |
65
+ | guard | [@helpers4/guard](https://www.npmjs.com/package/@helpers4/guard) | [Source](https://github.com/helpers4/helpers4/tree/main/helpers/guard) | Runtime type guard utilities |
64
66
  | id | [@helpers4/id](https://www.npmjs.com/package/@helpers4/id) | [Source](https://github.com/helpers4/helpers4/tree/main/helpers/id) | Unique identifier generation utilities |
65
67
  | markdown | [@helpers4/markdown](https://www.npmjs.com/package/@helpers4/markdown) | [Source](https://github.com/helpers4/helpers4/tree/main/helpers/markdown) | Markdown formatting and escaping utilities |
66
68
  | node | [@helpers4/node](https://www.npmjs.com/package/@helpers4/node) | [Source](https://github.com/helpers4/helpers4/tree/main/helpers/node) | Node.js-specific utilities |
@@ -69,7 +71,7 @@ A set of helpers for working with URLs.
69
71
  | observable | [@helpers4/observable](https://www.npmjs.com/package/@helpers4/observable) | [Source](https://github.com/helpers4/helpers4/tree/main/helpers/observable) | Observable utilities and combinators |
70
72
  | promise | [@helpers4/promise](https://www.npmjs.com/package/@helpers4/promise) | [Source](https://github.com/helpers4/helpers4/tree/main/helpers/promise) | Promise utilities and error handling |
71
73
  | string | [@helpers4/string](https://www.npmjs.com/package/@helpers4/string) | [Source](https://github.com/helpers4/helpers4/tree/main/helpers/string) | String manipulation and formatting utilities |
72
- | type | [@helpers4/type](https://www.npmjs.com/package/@helpers4/type) | [Source](https://github.com/helpers4/helpers4/tree/main/helpers/type) | Type checking and validation utilities |
74
+ | type | [@helpers4/type](https://www.npmjs.com/package/@helpers4/type) | [Source](https://github.com/helpers4/helpers4/tree/main/helpers/type) | Compile-time TypeScript utility types |
73
75
  | url | [@helpers4/url](https://www.npmjs.com/package/@helpers4/url) | [Source](https://github.com/helpers4/helpers4/tree/main/helpers/url) | URL parsing and manipulation utilities |
74
76
  | version | [@helpers4/version](https://www.npmjs.com/package/@helpers4/version) | [Source](https://github.com/helpers4/helpers4/tree/main/helpers/version) | Version string manipulation utilities |
75
77
  <!-- /AUTOMATIC-CATEGORIES-TABLE -->
package/lib/index.d.ts CHANGED
@@ -4,30 +4,31 @@
4
4
  * SPDX-License-Identifier: LGPL-3.0-or-later
5
5
  */
6
6
  /**
7
- * Removes all entries with falsy values (`false`, `null`, `undefined`, `0`, `""`, `NaN`) from an object.
8
- * @param obj - The source object
9
- * @returns A new object containing only entries with truthy values
7
+ * Creates a deep copy of an object or array.
8
+ *
9
+ * Recursively clones own enumerable string keys. `Date` instances are
10
+ * reconstructed with the same epoch value. Prototype-polluting keys
11
+ * (`__proto__`, `constructor`, `prototype`) are silently skipped.
12
+ * Does **not** handle circular references.
13
+ *
14
+ * **Limitation:** symbol-keyed own properties are **not** copied — only string
15
+ * keys are processed. Use `structuredClone` if symbol propagation is required.
16
+ *
17
+ * @param obj - The value to clone
18
+ * @returns A deep copy of `obj`
10
19
  * @example
11
- * compact({ a: 1, b: null, c: '', d: 0, e: 'hello' })
12
- * // => { a: 1, e: 'hello' }
13
- * @since 2.0.0
14
- */
15
- declare function compact<T extends Record<string, unknown>>(obj: T): Partial<T>;
16
- declare function compact(obj: undefined): undefined;
17
- declare function compact(obj: null): null;
18
-
19
- /**
20
- * This file is part of helpers4.
21
- * Copyright (C) 2025 baxyz
22
- * SPDX-License-Identifier: LGPL-3.0-or-later
23
- */
24
- /**
25
- * Creates a deep copy of an object or array
26
- * @param obj - The object to clone
27
- * @returns Deep cloned object
20
+ * cloneDeep({ a: { b: [1, 2] } })
21
+ * // => { a: { b: [1, 2] } } — fully independent copy
22
+ *
23
+ * cloneDeep(new Date('2024-01-01'))
24
+ * // => Date with the same timestamp
25
+ *
26
+ * // Primitives pass through unchanged
27
+ * cloneDeep(42) // => 42
28
+ * cloneDeep(null) // => null
28
29
  * @since 1.9.0
29
30
  */
30
- declare function deepClone<T>(obj: T): T;
31
+ declare function cloneDeep<T>(obj: T): T;
31
32
 
32
33
  /**
33
34
  * This file is part of helpers4.
@@ -35,21 +36,19 @@ declare function deepClone<T>(obj: T): T;
35
36
  * SPDX-License-Identifier: LGPL-3.0-or-later
36
37
  */
37
38
  /**
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
48
- * @since 1.9.0
39
+ * Removes all entries with falsy values (`false`, `null`, `undefined`, `0`, `""`, `NaN`) from an object.
40
+ * Own keys that are prototype-polluting strings (`__proto__`, `constructor`, `prototype`) are
41
+ * silently skipped.
42
+ * @param obj - The source object
43
+ * @returns A new object containing only entries with truthy values
44
+ * @example
45
+ * compact({ a: 1, b: null, c: '', d: 0, e: 'hello' })
46
+ * // => { a: 1, e: 'hello' }
47
+ * @since 2.0.0
49
48
  */
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;
49
+ declare function compact<T extends Record<string, unknown>>(obj: T): Partial<T>;
50
+ declare function compact(obj: undefined): undefined;
51
+ declare function compact(obj: null): null;
53
52
 
54
53
  /**
55
54
  * This file is part of helpers4.
@@ -168,30 +167,109 @@ declare function equalsShallow(objA: unknown, objB: unknown): boolean;
168
167
  * Copyright (C) 2025 baxyz
169
168
  * SPDX-License-Identifier: LGPL-3.0-or-later
170
169
  */
170
+ /**
171
+ * Keys that must never be set on a plain object (prototype-pollution guard).
172
+ * @ignore
173
+ */
174
+ declare const UNSAFE_KEY_LIST: readonly ["__proto__", "constructor", "prototype"];
175
+ /** @ignore */
176
+ type UnsafeKey = (typeof UNSAFE_KEY_LIST)[number];
177
+
178
+ /**
179
+ * This file is part of helpers4.
180
+ * Copyright (C) 2025 baxyz
181
+ * SPDX-License-Identifier: LGPL-3.0-or-later
182
+ */
183
+
184
+ /**
185
+ * Parses a `[n]` bracket index: numeric index strings become `number`, anything else stays `string`.
186
+ * @ignore
187
+ */
188
+ type BracketKey<S extends string> = S extends `${number}` ? number : S;
189
+ /**
190
+ * Parses consecutive bracket segments: `'[0][1]'` → `[0, 1]`.
191
+ * @ignore
192
+ */
193
+ type ParseBrackets<S extends string> = S extends `[${infer Idx}]${infer Rest}` ? [BracketKey<Idx>, ...ParseBrackets<Rest>] : [];
194
+ /**
195
+ * Parses a single dot-segment (no dots inside): `'arr[0]'` → `['arr', 0]`.
196
+ * @ignore
197
+ */
198
+ type ParseSegment<S extends string> = S extends `${infer Name}[${infer Idx}]${infer Rest}` ? Name extends '' ? [BracketKey<Idx>, ...ParseBrackets<Rest>] : [Name, BracketKey<Idx>, ...ParseBrackets<Rest>] : [S];
199
+ /**
200
+ * Converts a dot/bracket path string literal into a `PropertyKey` tuple.
201
+ *
202
+ * - Dot segments produce string keys: `'a.1'` → `['a', '1']`
203
+ * - Bracket segments produce number keys: `'a[1]'` → `['a', 1]`
204
+ *
205
+ * Limitation: edge-case paths handled specially at runtime (empty string `''`,
206
+ * bare `'.'`, consecutive dots `'a..b'`) are not modelled at the type level.
207
+ *
208
+ * @ignore
209
+ */
210
+ type ParseStringPath<S extends string> = string extends S ? readonly PropertyKey[] : S extends `${infer Head}.${infer Tail}` ? [...ParseSegment<Head>, ...ParseStringPath<Tail>] : ParseSegment<S>;
211
+ /**
212
+ * Normalises both path forms into a `PropertyKey` tuple for use with `DeepGet`/`DeepSet`.
213
+ * - `string` literal → parsed via `ParseStringPath`
214
+ * - `readonly PropertyKey[]` → used as-is
215
+ * @ignore
216
+ */
217
+ type ParsePath<P extends string | readonly PropertyKey[]> = P extends string ? ParseStringPath<P> : P extends readonly PropertyKey[] ? P : never;
218
+ /**
219
+ * Returns `true` if any key in the tuple is a prototype-polluting string.
220
+ * Used to make `set` return `T` unchanged (instead of a misleading `DeepSet` type)
221
+ * when the path is rejected by the runtime guard.
222
+ * @ignore
223
+ */
224
+ type ContainsUnsafeStringKey<Keys extends readonly PropertyKey[]> = Keys extends readonly [infer K, ...infer Rest extends readonly PropertyKey[]] ? K extends UnsafeKey ? true : ContainsUnsafeStringKey<Rest> : false;
225
+ /**
226
+ * Converts a union to an intersection: `A | B | C` → `A & B & C`.
227
+ * Used to derive the return type of `mergeDeep` from a tuple of source types.
228
+ * @ignore
229
+ */
230
+ type UnionToIntersection<U> = (U extends any ? (x: U) => void : never) extends (x: infer I) => void ? I : never;
231
+ /**
232
+ * Return type of `mergeDeep`: intersection of all source types.
233
+ *
234
+ * @ignore
235
+ * @example
236
+ * MergeResult<[{ a: number }, { b: string }]> // { a: number } & { b: string }
237
+ * MergeResult<[{ a: { b: number } }, { a: { c: string } }]> // { a: { b: number } & { c: string } }
238
+ */
239
+ type MergeResult<T extends object[]> = UnionToIntersection<T[number]>;
171
240
  /**
172
241
  * Resolves the value type at a given path within T.
173
242
  * Returns `unknown` when the path doesn't match the shape of T.
174
243
  *
175
- * @internal
244
+ * @ignore
176
245
  * @example
177
246
  * DeepGet<{ a: { b: number } }, ['a', 'b']> // => number
178
247
  * DeepGet<{ a: { b: number } }, ['a', 'x']> // => unknown
179
248
  * DeepGet<{ a: number }, []> // => { a: number }
180
249
  */
181
250
  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;
251
+ /**
252
+ * Builds a brand-new nested object type from the remaining path segments and a leaf value type.
253
+ * Used by `DeepSet` once a path segment no longer matches an existing key on T.
254
+ * @ignore
255
+ */
256
+ type BuildPath<Path extends readonly PropertyKey[], V> = Path extends readonly [infer K extends PropertyKey, ...infer Rest extends readonly PropertyKey[]] ? Record<K, BuildPath<Rest, V>> : V;
182
257
  /**
183
258
  * 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.
259
+ * When a key in Path is not present at the corresponding level of T, that level (and
260
+ * everything below it) is added as a new field instead of resolving to `never` — matching
261
+ * the runtime behavior of `set`, which creates intermediate objects as needed.
186
262
  *
187
- * @internal
263
+ * @ignore
188
264
  * @example
189
265
  * DeepSet<{ a: { b: number } }, ['a', 'b'], string>
190
266
  * // => { a: { b: string } }
267
+ * DeepSet<{ a: { b: number } }, ['a', 'c'], string>
268
+ * // => { a: { b: number } & { c: string } }
191
269
  */
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 ? {
270
+ type DeepSet<T, Path extends readonly PropertyKey[], V> = Path extends readonly [] ? V : Path extends readonly [infer K extends PropertyKey, ...infer Rest extends readonly PropertyKey[]] ? K extends keyof T ? {
193
271
  [P in keyof T]: P extends K ? DeepSet<T[K], Rest, V> : T[P];
194
- } : never : never;
272
+ } : T & Record<K, BuildPath<Rest, V>> : never;
195
273
 
196
274
  /**
197
275
  * This file is part of helpers4.
@@ -205,29 +283,36 @@ type DeepSet<T, Path extends readonly PropertyKey[], V> = Path extends readonly
205
283
  * **Two path forms are supported:**
206
284
  *
207
285
  * 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.
286
+ * are both accepted and mixed freely.
287
+ * - Dot segments are always string keys: `'a.1'` → key `'1'` (string).
288
+ * - Bracket segments are always number keys: `'a[1]'` → key `1` (number).
289
+ * - String literal paths give **full compile-time type inference** on the return type.
290
+ * - Dynamic (non-literal) strings return `unknown`.
210
291
  *
211
292
  * 2. **Key array** (`PropertyKey[]`) — explicit array of `string | number | symbol` keys,
212
293
  * no parsing performed. Enables symbol-keyed traversal and compile-time type inference:
213
294
  * `get(obj, ['a', 'b'] as const)` infers the return type from the path.
214
295
  *
296
+ * Both forms support **all objects** (plain objects, arrays, class instances).
297
+ * Symbol keys are only reachable via the key-array form.
298
+ *
215
299
  * @param obj - The object to read from
216
- * @param path - Dot/bracket-notation string or explicit `PropertyKey[]`
300
+ * @param path - Dot/bracket-notation string literal or explicit `PropertyKey[]`
217
301
  * @param defaultValue - Returned when the path is absent or resolves to `undefined`
218
302
  * @returns The value at the path, or `defaultValue`
219
303
  * @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)
304
+ * get({ a: { b: { c: 42 } } }, 'a.b.c') // => 42 (inferred: number | undefined)
305
+ * get({ layers: [{ name: 'bg' }] }, 'layers[0].name') // => 'bg' (inferred: string | undefined)
306
+ * get({ a: { b: 0 } }, 'a.b', 99) // => 0 (falsy values returned as-is)
223
307
  * get({ a: 1 }, 'b.c', 'default') // => 'default'
224
308
  *
225
309
  * const id = Symbol('id')
226
- * get({ [id]: 'alice' }, [id]) // => 'alice' (symbol key)
310
+ * get({ [id]: 'alice' }, [id]) // => 'alice' (symbol key, inferred: string | undefined)
227
311
  * @since 1.9.0
228
312
  */
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;
313
+ declare function get<D>(obj: null | undefined, path: string | readonly PropertyKey[], defaultValue: D): D;
314
+ declare function get(obj: null | undefined, path: string | readonly PropertyKey[]): undefined;
315
+ declare function get<T extends object, const P extends string | readonly PropertyKey[]>(obj: T | null | undefined, path: P, defaultValue?: DeepGet<T, ParsePath<P>>): DeepGet<T, ParsePath<P>> | undefined;
231
316
 
232
317
  /**
233
318
  * This file is part of helpers4.
@@ -239,6 +324,9 @@ declare function get<T extends object, const Path extends readonly PropertyKey[]
239
324
  *
240
325
  * A thin, typed wrapper around `Object.groupBy` (ES2024) that works on
241
326
  * older targets and provides stricter return-type inference.
327
+ * `null` and `undefined` are treated as empty arrays and return `{}`.
328
+ * Items whose computed key is a prototype-polluting string (`__proto__`,
329
+ * `constructor`, `prototype`) are silently skipped.
242
330
  *
243
331
  * @param items - The array to group
244
332
  * @param keyFn - Function that returns the group key for each item
@@ -246,9 +334,11 @@ declare function get<T extends object, const Path extends readonly PropertyKey[]
246
334
  * @example
247
335
  * groupBy([1, 2, 3, 4], n => n % 2 === 0 ? 'even' : 'odd')
248
336
  * // => { odd: [1, 3], even: [2, 4] }
337
+ * groupBy(null, n => n % 2 === 0 ? 'even' : 'odd')
338
+ * // => {}
249
339
  * @since 2.0.0
250
340
  */
251
- declare function groupBy<T, K extends PropertyKey>(items: readonly T[], keyFn: (item: T) => K): Partial<Record<K, T[]>>;
341
+ declare function groupBy<T, K extends PropertyKey>(items: readonly T[] | null | undefined, keyFn: (item: T) => K): Partial<Record<K, T[]>>;
252
342
 
253
343
  /**
254
344
  * This file is part of helpers4.
@@ -258,6 +348,9 @@ declare function groupBy<T, K extends PropertyKey>(items: readonly T[], keyFn: (
258
348
  /**
259
349
  * Returns a new object with keys and values swapped.
260
350
  * If multiple keys share the same value, the last one wins.
351
+ * `null` and `undefined` are treated as empty objects and return `{}`.
352
+ * Entries whose source key **or** value is a prototype-polluting string
353
+ * (`__proto__`, `constructor`, `prototype`) are silently skipped.
261
354
  *
262
355
  * @param obj - The object whose keys and values are to be swapped
263
356
  * @returns A new object with values as keys and original keys as values
@@ -267,9 +360,12 @@ declare function groupBy<T, K extends PropertyKey>(items: readonly T[], keyFn: (
267
360
  *
268
361
  * invert({ one: 1, two: 2 })
269
362
  * // => { 1: 'one', 2: 'two' }
363
+ *
364
+ * invert(null)
365
+ * // => {}
270
366
  * @since 2.0.0
271
367
  */
272
- declare function invert<K extends string, V extends PropertyKey>(obj: Record<K, V>): Record<V, K>;
368
+ declare function invert<K extends string, V extends PropertyKey>(obj: Record<K, V> | null | undefined): Record<V, K>;
273
369
 
274
370
  /**
275
371
  * This file is part of helpers4.
@@ -278,19 +374,22 @@ declare function invert<K extends string, V extends PropertyKey>(obj: Record<K,
278
374
  */
279
375
  /**
280
376
  * Checks if a plain object has no own enumerable string-keyed properties.
377
+ * `null` and `undefined` are treated as empty objects and return `true`.
281
378
  *
282
379
  * Symbol-keyed properties are not counted. Use `Object.getOwnPropertySymbols`
283
380
  * separately if symbol keys matter for your use case.
284
381
  *
285
382
  * @param value - The object to check
286
- * @returns `true` if the object has no own enumerable string-keyed properties
383
+ * @returns `true` if the object has no own enumerable string-keyed properties, or if `value` is `null`/`undefined`
287
384
  * @example
288
385
  * isEmpty({}) // => true
386
+ * isEmpty(null) // => true
387
+ * isEmpty(undefined) // => true
289
388
  * isEmpty({ a: 1 }) // => false
290
389
  * isEmpty({ a: undefined }) // => false (key exists)
291
390
  * @since 2.0.3
292
391
  */
293
- declare function isEmpty(value: Record<PropertyKey, unknown>): boolean;
392
+ declare function isEmpty(value: Record<PropertyKey, unknown> | null | undefined): boolean;
294
393
 
295
394
  /**
296
395
  * This file is part of helpers4.
@@ -299,18 +398,22 @@ declare function isEmpty(value: Record<PropertyKey, unknown>): boolean;
299
398
  */
300
399
  /**
301
400
  * Checks if a plain object has at least one own enumerable string-keyed property.
401
+ * `null` and `undefined` are treated as empty objects and return `false`.
302
402
  *
303
403
  * Symbol-keyed properties are not counted. Use `Object.getOwnPropertySymbols`
304
404
  * separately if symbol keys matter for your use case.
305
405
  *
306
406
  * @param value - The object to check
307
- * @returns `true` if the object has at least one own enumerable string-keyed property
407
+ * @returns `true` if the object has at least one own enumerable string-keyed property; `false` for empty, `null`, or `undefined`.
408
+ * Acts as a type guard: the `if` branch narrows `Record<PropertyKey, unknown> | null | undefined` to `Record<PropertyKey, unknown>`.
308
409
  * @example
309
410
  * isNonEmpty({ a: 1 }) // => true
310
411
  * isNonEmpty({}) // => false
412
+ * isNonEmpty(null) // => false
413
+ * isNonEmpty(undefined) // => false
311
414
  * @since 2.0.3
312
415
  */
313
- declare function isNonEmpty(value: Record<PropertyKey, unknown>): boolean;
416
+ declare function isNonEmpty(value: Record<PropertyKey, unknown> | null | undefined): value is Record<PropertyKey, unknown>;
314
417
 
315
418
  /**
316
419
  * This file is part of helpers4.
@@ -325,7 +428,8 @@ declare function isNonEmpty(value: Record<PropertyKey, unknown>): boolean;
325
428
  * when `mapKey` is omitted the original keys are preserved.
326
429
  *
327
430
  * Note: if two different keys map to the same output key the last one wins
328
- * (insertion order).
431
+ * (insertion order). Entries whose mapped key is a prototype-polluting string
432
+ * (`__proto__`, `constructor`, `prototype`) are silently skipped.
329
433
  *
330
434
  * @param obj - The source object
331
435
  * @param mapValue - Callback called with `(value, key)` for each entry.
@@ -344,7 +448,69 @@ declare function isNonEmpty(value: Record<PropertyKey, unknown>): boolean;
344
448
  * // => { A: 10, B: 20 }
345
449
  * @since 2.0.0
346
450
  */
347
- declare function map<TObj extends Record<string, unknown>, TVal = TObj[keyof TObj], TKey extends PropertyKey = keyof TObj>(obj: TObj, mapValue?: (value: TObj[keyof TObj], key: keyof TObj) => TVal, mapKey?: (key: keyof TObj, value: TObj[keyof TObj]) => TKey): Record<TKey, TVal>;
451
+ declare function map<TObj extends Record<string, unknown>, TVal = TObj[keyof TObj], TKey extends PropertyKey = keyof TObj>(obj: TObj | null | undefined, mapValue?: (value: TObj[keyof TObj], key: keyof TObj) => TVal, mapKey?: (key: keyof TObj, value: TObj[keyof TObj]) => TKey): Record<TKey, TVal>;
452
+
453
+ /**
454
+ * This file is part of helpers4.
455
+ * Copyright (C) 2025 baxyz
456
+ * SPDX-License-Identifier: LGPL-3.0-or-later
457
+ */
458
+
459
+ /**
460
+ * Merges two or more objects deeply, returning a **new** object without mutating any input.
461
+ *
462
+ * Sources are applied left to right. For each key:
463
+ * - If both sides are plain objects → merged recursively into a new object.
464
+ * - Otherwise → **later source wins** (the earlier value is overwritten).
465
+ * - `undefined` in a source **never overwrites** an existing value.
466
+ * - `null` in a source **does overwrite** and prevents a later source from
467
+ * deep-merging into that key: `mergeDeep({ a: { x: 1 } }, { a: null }, { a: { y: 2 } })`
468
+ * → `{ a: { y: 2 } }` — `x: 1` is permanently lost.
469
+ * - Arrays, class instances, and all non-plain-object values are **replaced**, not merged.
470
+ *
471
+ * Own enumerable string and symbol keys of each source are processed at the top level;
472
+ * inherited and non-enumerable properties are skipped. Prototype-polluting keys
473
+ * (`__proto__`, `constructor`, `prototype`) are silently ignored.
474
+ * **Note:** symbol keys inside nested plain-object values are not preserved — they are
475
+ * lost when those values are deep-cloned. Only top-level symbol keys survive.
476
+ *
477
+ * **TypeScript return type — intersection semantics**
478
+ *
479
+ * The return type is `A & B & C …` (intersection of all source types). This is accurate
480
+ * when keys are disjoint or share the same type:
481
+ * ```ts
482
+ * mergeDeep({ a: 1 }, { b: 'x' }) // { a: number } & { b: string } ✓
483
+ * mergeDeep({ a: { b: 1 } }, { a: { c: 2 } }) // { a: { b: number } & { c: number } } ✓
484
+ * ```
485
+ * When the same key carries **incompatible types** across sources, the intersection
486
+ * resolves to `never` for that key — TypeScript surfaces the conflict rather than
487
+ * silently picking a type:
488
+ * ```ts
489
+ * mergeDeep({ a: 1 }, { a: 'x' }) // { a: never } ← type conflict detected
490
+ * ```
491
+ * At runtime the later source always wins (`'x'`), but the `never` type signals that
492
+ * the caller should align their source types. If intentional, cast the result.
493
+ *
494
+ * @param sources - Two or more objects to merge (none are mutated)
495
+ * @returns A new object that is the deep merge of all sources
496
+ * @example
497
+ * mergeDeep({ a: 1, b: { c: 2 } }, { b: { d: 3 }, e: 4 })
498
+ * // => { a: 1, b: { c: 2, d: 3 }, e: 4 }
499
+ *
500
+ * // Later source wins for non-object values
501
+ * mergeDeep({ a: 1, arr: [1, 2] }, { a: 2, arr: [3] })
502
+ * // => { a: 2, arr: [3] }
503
+ *
504
+ * // undefined never overwrites
505
+ * mergeDeep({ a: 1 }, { a: undefined })
506
+ * // => { a: 1 }
507
+ *
508
+ * // Three sources, left to right
509
+ * mergeDeep({ x: 1 }, { y: 2 }, { x: 99, z: 3 })
510
+ * // => { x: 99, y: 2, z: 3 }
511
+ * @since 1.9.0
512
+ */
513
+ declare function mergeDeep<T extends [object, ...object[]]>(...sources: [...T]): MergeResult<T>;
348
514
 
349
515
  /**
350
516
  * This file is part of helpers4.
@@ -372,6 +538,8 @@ declare function omit(obj: null, keys: readonly string[]): null;
372
538
  */
373
539
  /**
374
540
  * Creates a new object with only the specified keys.
541
+ * Keys that are prototype-polluting strings (`__proto__`, `constructor`, `prototype`) are
542
+ * silently skipped.
375
543
  * @param obj - The source object
376
544
  * @param keys - The keys to pick
377
545
  * @returns A new object with only the picked keys
@@ -446,47 +614,44 @@ declare function safeJsonParse<T>(input: string, fallback: T): T;
446
614
  /**
447
615
  * Sets a value in an object at the given path, creating intermediate objects as needed.
448
616
  *
449
- * **Three path forms are supported:**
617
+ * **Two path forms are supported:**
450
618
  *
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"`).
619
+ * 1. **String path** — dot notation and bracket notation, mixed freely.
620
+ * - Dot segments are always string keys: `'layers.1.name'` → keys `['layers', '1', 'name']`.
621
+ * - Bracket segments are always number keys: `'layers[1].name'` → keys `['layers', 1, 'name']`.
622
+ * - String literal paths give **full compile-time type inference** on the return type.
623
+ * - Dynamic (non-literal) strings return `T` (same object type).
453
624
  *
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"`.
625
+ * 2. **Key array** (`PropertyKey[]`) — explicit array of `string | number | symbol` keys,
626
+ * no parsing performed. Enables symbol-keyed access and precise return-type inference.
457
627
  *
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')]`.
628
+ * Both forms support **all objects** (plain objects, arrays, class instances).
629
+ * Symbol keys are only reachable via the key-array form.
461
630
  *
462
631
  * Intermediate nodes that are absent, `null`, or not an object are replaced with `{}`.
463
632
  * Any path containing a string segment equal to `__proto__`, `constructor`, or `prototype`
464
633
  * is rejected and the original object is returned unchanged (prototype-pollution guard).
465
634
  *
466
635
  * @param obj - The object to mutate
467
- * @param path - Dot/bracket-notation string or explicit `PropertyKey[]`
636
+ * @param path - Dot/bracket-notation string literal or explicit `PropertyKey[]`
468
637
  * @param value - Value to assign at the path
469
- * @returns The mutated object (same reference)
638
+ * @returns The mutated object (same reference, narrowed type)
470
639
  * @example
471
- * // 1. Dot notation — "1" stays a string key
640
+ * // Dot notation — "1" stays a string key
472
641
  * set({}, 'a.b.c', 42)
473
642
  * // => { a: { b: { c: 42 } } }
474
643
  *
475
- * set({}, 'layers.1.name', 'bg')
476
- * // => { layers: { '1': { name: 'bg' } } }
477
- *
478
- * // 2. Bracket notation — [1] becomes a number key
644
+ * // Bracket notation — [1] becomes a number key
479
645
  * set({ layers: [{}, { name: 'old' }] }, 'layers[1].name', 'bg')
480
646
  * // => { layers: [{}, { name: 'bg' }] }
481
647
  *
482
- * // 3. Key array — supports symbols
648
+ * // Key array — supports symbols
483
649
  * const id = Symbol('id')
484
650
  * set({}, ['user', id], 'alice')
485
651
  * // => { user: { [id]: 'alice' } }
486
652
  * @since 1.9.0
487
653
  */
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>;
654
+ declare function set<T extends object, const P extends string | readonly PropertyKey[], V extends DeepGet<T, ParsePath<P>>>(obj: T, path: P, value: V): string extends P ? T : readonly PropertyKey[] extends P ? T : ContainsUnsafeStringKey<ParsePath<P>> extends true ? T : DeepSet<T, ParsePath<P>, V>;
490
655
 
491
- export { compact, deepClone, deepMerge, diff, equalsDeep, equalsShallow, get, groupBy, invert, isEmpty, isNonEmpty, map, omit, pick, removeUndefinedNull, safeJsonParse, set };
656
+ export { cloneDeep, compact, diff, equalsDeep, equalsShallow, get, groupBy, invert, isEmpty, isNonEmpty, map, mergeDeep, omit, pick, removeUndefinedNull, safeJsonParse, set };
492
657
  export type { DiffOptions, DiffResult };