@helpers4/object 2.0.3 → 2.1.0

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
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,15 +36,17 @@ 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
- * @param target - The target object
40
- * @param sources - The source objects to merge
41
- * @returns The merged object
42
- * @since 1.9.0
39
+ * Removes all entries with falsy values (`false`, `null`, `undefined`, `0`, `""`, `NaN`) from an object.
40
+ * @param obj - The source object
41
+ * @returns A new object containing only entries with truthy values
42
+ * @example
43
+ * compact({ a: 1, b: null, c: '', d: 0, e: 'hello' })
44
+ * // => { a: 1, e: 'hello' }
45
+ * @since 2.0.0
43
46
  */
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;
47
+ declare function compact<T extends Record<string, unknown>>(obj: T): Partial<T>;
48
+ declare function compact(obj: undefined): undefined;
49
+ declare function compact(obj: null): null;
47
50
 
48
51
  /**
49
52
  * This file is part of helpers4.
@@ -163,14 +166,142 @@ declare function equalsShallow(objA: unknown, objB: unknown): boolean;
163
166
  * SPDX-License-Identifier: LGPL-3.0-or-later
164
167
  */
165
168
  /**
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
169
+ * Keys that must never be set on a plain object (prototype-pollution guard).
170
+ * @ignore
171
+ */
172
+ declare const UNSAFE_KEY_LIST: readonly ["__proto__", "constructor", "prototype"];
173
+ /** @ignore */
174
+ type UnsafeKey = (typeof UNSAFE_KEY_LIST)[number];
175
+
176
+ /**
177
+ * This file is part of helpers4.
178
+ * Copyright (C) 2025 baxyz
179
+ * SPDX-License-Identifier: LGPL-3.0-or-later
180
+ */
181
+
182
+ /**
183
+ * Parses a `[n]` bracket index: numeric index strings become `number`, anything else stays `string`.
184
+ * @ignore
185
+ */
186
+ type BracketKey<S extends string> = S extends `${number}` ? number : S;
187
+ /**
188
+ * Parses consecutive bracket segments: `'[0][1]'` → `[0, 1]`.
189
+ * @ignore
190
+ */
191
+ type ParseBrackets<S extends string> = S extends `[${infer Idx}]${infer Rest}` ? [BracketKey<Idx>, ...ParseBrackets<Rest>] : [];
192
+ /**
193
+ * Parses a single dot-segment (no dots inside): `'arr[0]'` → `['arr', 0]`.
194
+ * @ignore
195
+ */
196
+ 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];
197
+ /**
198
+ * Converts a dot/bracket path string literal into a `PropertyKey` tuple.
199
+ *
200
+ * - Dot segments produce string keys: `'a.1'` → `['a', '1']`
201
+ * - Bracket segments produce number keys: `'a[1]'` → `['a', 1]`
202
+ *
203
+ * Limitation: edge-case paths handled specially at runtime (empty string `''`,
204
+ * bare `'.'`, consecutive dots `'a..b'`) are not modelled at the type level.
205
+ *
206
+ * @ignore
207
+ */
208
+ type ParseStringPath<S extends string> = string extends S ? readonly PropertyKey[] : S extends `${infer Head}.${infer Tail}` ? [...ParseSegment<Head>, ...ParseStringPath<Tail>] : ParseSegment<S>;
209
+ /**
210
+ * Normalises both path forms into a `PropertyKey` tuple for use with `DeepGet`/`DeepSet`.
211
+ * - `string` literal → parsed via `ParseStringPath`
212
+ * - `readonly PropertyKey[]` → used as-is
213
+ * @ignore
214
+ */
215
+ type ParsePath<P extends string | readonly PropertyKey[]> = P extends string ? ParseStringPath<P> : P extends readonly PropertyKey[] ? P : never;
216
+ /**
217
+ * Returns `true` if any key in the tuple is a prototype-polluting string.
218
+ * Used to make `set` return `T` unchanged (instead of a misleading `DeepSet` type)
219
+ * when the path is rejected by the runtime guard.
220
+ * @ignore
221
+ */
222
+ type ContainsUnsafeStringKey<Keys extends readonly PropertyKey[]> = Keys extends readonly [infer K, ...infer Rest extends readonly PropertyKey[]] ? K extends UnsafeKey ? true : ContainsUnsafeStringKey<Rest> : false;
223
+ /**
224
+ * Converts a union to an intersection: `A | B | C` → `A & B & C`.
225
+ * Used to derive the return type of `mergeDeep` from a tuple of source types.
226
+ * @ignore
227
+ */
228
+ type UnionToIntersection<U> = (U extends any ? (x: U) => void : never) extends (x: infer I) => void ? I : never;
229
+ /**
230
+ * Return type of `mergeDeep`: intersection of all source types.
231
+ *
232
+ * @ignore
233
+ * @example
234
+ * MergeResult<[{ a: number }, { b: string }]> // { a: number } & { b: string }
235
+ * MergeResult<[{ a: { b: number } }, { a: { c: string } }]> // { a: { b: number } & { c: string } }
236
+ */
237
+ type MergeResult<T extends object[]> = UnionToIntersection<T[number]>;
238
+ /**
239
+ * Resolves the value type at a given path within T.
240
+ * Returns `unknown` when the path doesn't match the shape of T.
241
+ *
242
+ * @ignore
243
+ * @example
244
+ * DeepGet<{ a: { b: number } }, ['a', 'b']> // => number
245
+ * DeepGet<{ a: { b: number } }, ['a', 'x']> // => unknown
246
+ * DeepGet<{ a: number }, []> // => { a: number }
247
+ */
248
+ 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;
249
+ /**
250
+ * Produces the type of T after setting the value at Path to V.
251
+ * Resolves to `never` when a key in Path is not present in the corresponding level of T,
252
+ * surfacing invalid paths at the call site rather than silently returning T unchanged.
253
+ *
254
+ * @ignore
255
+ * @example
256
+ * DeepSet<{ a: { b: number } }, ['a', 'b'], string>
257
+ * // => { a: { b: string } }
258
+ */
259
+ 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 ? {
260
+ [P in keyof T]: P extends K ? DeepSet<T[K], Rest, V> : T[P];
261
+ } : never : never;
262
+
263
+ /**
264
+ * This file is part of helpers4.
265
+ * Copyright (C) 2025 baxyz
266
+ * SPDX-License-Identifier: LGPL-3.0-or-later
267
+ */
268
+
269
+ /**
270
+ * Gets a value from an object using a dot/bracket-notated path or explicit key array.
271
+ *
272
+ * **Two path forms are supported:**
273
+ *
274
+ * 1. **String path** — dot notation (`'a.b.c'`) and bracket notation (`'layers[1].name'`)
275
+ * are both accepted and mixed freely.
276
+ * - Dot segments are always string keys: `'a.1'` → key `'1'` (string).
277
+ * - Bracket segments are always number keys: `'a[1]'` → key `1` (number).
278
+ * - String literal paths give **full compile-time type inference** on the return type.
279
+ * - Dynamic (non-literal) strings return `unknown`.
280
+ *
281
+ * 2. **Key array** (`PropertyKey[]`) — explicit array of `string | number | symbol` keys,
282
+ * no parsing performed. Enables symbol-keyed traversal and compile-time type inference:
283
+ * `get(obj, ['a', 'b'] as const)` infers the return type from the path.
284
+ *
285
+ * Both forms support **all objects** (plain objects, arrays, class instances).
286
+ * Symbol keys are only reachable via the key-array form.
287
+ *
288
+ * @param obj - The object to read from
289
+ * @param path - Dot/bracket-notation string literal or explicit `PropertyKey[]`
290
+ * @param defaultValue - Returned when the path is absent or resolves to `undefined`
291
+ * @returns The value at the path, or `defaultValue`
292
+ * @example
293
+ * get({ a: { b: { c: 42 } } }, 'a.b.c') // => 42 (inferred: number | undefined)
294
+ * get({ layers: [{ name: 'bg' }] }, 'layers[0].name') // => 'bg' (inferred: string | undefined)
295
+ * get({ a: { b: 0 } }, 'a.b', 99) // => 0 (falsy values returned as-is)
296
+ * get({ a: 1 }, 'b.c', 'default') // => 'default'
297
+ *
298
+ * const id = Symbol('id')
299
+ * get({ [id]: 'alice' }, [id]) // => 'alice' (symbol key, inferred: string | undefined)
171
300
  * @since 1.9.0
172
301
  */
173
- declare function get<T = unknown>(obj: unknown, path: string, defaultValue?: T): T | undefined;
302
+ declare function get<D>(obj: null | undefined, path: string | readonly PropertyKey[], defaultValue: D): D;
303
+ declare function get(obj: null | undefined, path: string | readonly PropertyKey[]): undefined;
304
+ 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;
174
305
 
175
306
  /**
176
307
  * This file is part of helpers4.
@@ -182,6 +313,9 @@ declare function get<T = unknown>(obj: unknown, path: string, defaultValue?: T):
182
313
  *
183
314
  * A thin, typed wrapper around `Object.groupBy` (ES2024) that works on
184
315
  * older targets and provides stricter return-type inference.
316
+ * `null` and `undefined` are treated as empty arrays and return `{}`.
317
+ * Items whose computed key is a prototype-polluting string (`__proto__`,
318
+ * `constructor`, `prototype`) are silently skipped.
185
319
  *
186
320
  * @param items - The array to group
187
321
  * @param keyFn - Function that returns the group key for each item
@@ -189,9 +323,11 @@ declare function get<T = unknown>(obj: unknown, path: string, defaultValue?: T):
189
323
  * @example
190
324
  * groupBy([1, 2, 3, 4], n => n % 2 === 0 ? 'even' : 'odd')
191
325
  * // => { odd: [1, 3], even: [2, 4] }
326
+ * groupBy(null, n => n % 2 === 0 ? 'even' : 'odd')
327
+ * // => {}
192
328
  * @since 2.0.0
193
329
  */
194
- declare function groupBy<T, K extends PropertyKey>(items: readonly T[], keyFn: (item: T) => K): Partial<Record<K, T[]>>;
330
+ declare function groupBy<T, K extends PropertyKey>(items: readonly T[] | null | undefined, keyFn: (item: T) => K): Partial<Record<K, T[]>>;
195
331
 
196
332
  /**
197
333
  * This file is part of helpers4.
@@ -201,6 +337,9 @@ declare function groupBy<T, K extends PropertyKey>(items: readonly T[], keyFn: (
201
337
  /**
202
338
  * Returns a new object with keys and values swapped.
203
339
  * If multiple keys share the same value, the last one wins.
340
+ * `null` and `undefined` are treated as empty objects and return `{}`.
341
+ * Entries whose source key **or** value is a prototype-polluting string
342
+ * (`__proto__`, `constructor`, `prototype`) are silently skipped.
204
343
  *
205
344
  * @param obj - The object whose keys and values are to be swapped
206
345
  * @returns A new object with values as keys and original keys as values
@@ -210,9 +349,12 @@ declare function groupBy<T, K extends PropertyKey>(items: readonly T[], keyFn: (
210
349
  *
211
350
  * invert({ one: 1, two: 2 })
212
351
  * // => { 1: 'one', 2: 'two' }
352
+ *
353
+ * invert(null)
354
+ * // => {}
213
355
  * @since 2.0.0
214
356
  */
215
- declare function invert<K extends string, V extends PropertyKey>(obj: Record<K, V>): Record<V, K>;
357
+ declare function invert<K extends string, V extends PropertyKey>(obj: Record<K, V> | null | undefined): Record<V, K>;
216
358
 
217
359
  /**
218
360
  * This file is part of helpers4.
@@ -221,19 +363,22 @@ declare function invert<K extends string, V extends PropertyKey>(obj: Record<K,
221
363
  */
222
364
  /**
223
365
  * Checks if a plain object has no own enumerable string-keyed properties.
366
+ * `null` and `undefined` are treated as empty objects and return `true`.
224
367
  *
225
368
  * Symbol-keyed properties are not counted. Use `Object.getOwnPropertySymbols`
226
369
  * separately if symbol keys matter for your use case.
227
370
  *
228
371
  * @param value - The object to check
229
- * @returns `true` if the object has no own enumerable string-keyed properties
372
+ * @returns `true` if the object has no own enumerable string-keyed properties, or if `value` is `null`/`undefined`
230
373
  * @example
231
374
  * isEmpty({}) // => true
375
+ * isEmpty(null) // => true
376
+ * isEmpty(undefined) // => true
232
377
  * isEmpty({ a: 1 }) // => false
233
378
  * isEmpty({ a: undefined }) // => false (key exists)
234
379
  * @since 2.0.3
235
380
  */
236
- declare function isEmpty(value: Record<PropertyKey, unknown>): boolean;
381
+ declare function isEmpty(value: Record<PropertyKey, unknown> | null | undefined): boolean;
237
382
 
238
383
  /**
239
384
  * This file is part of helpers4.
@@ -242,18 +387,22 @@ declare function isEmpty(value: Record<PropertyKey, unknown>): boolean;
242
387
  */
243
388
  /**
244
389
  * Checks if a plain object has at least one own enumerable string-keyed property.
390
+ * `null` and `undefined` are treated as empty objects and return `false`.
245
391
  *
246
392
  * Symbol-keyed properties are not counted. Use `Object.getOwnPropertySymbols`
247
393
  * separately if symbol keys matter for your use case.
248
394
  *
249
395
  * @param value - The object to check
250
- * @returns `true` if the object has at least one own enumerable string-keyed property
396
+ * @returns `true` if the object has at least one own enumerable string-keyed property; `false` for empty, `null`, or `undefined`.
397
+ * Acts as a type guard: the `if` branch narrows `Record<PropertyKey, unknown> | null | undefined` to `Record<PropertyKey, unknown>`.
251
398
  * @example
252
399
  * isNonEmpty({ a: 1 }) // => true
253
400
  * isNonEmpty({}) // => false
401
+ * isNonEmpty(null) // => false
402
+ * isNonEmpty(undefined) // => false
254
403
  * @since 2.0.3
255
404
  */
256
- declare function isNonEmpty(value: Record<PropertyKey, unknown>): boolean;
405
+ declare function isNonEmpty(value: Record<PropertyKey, unknown> | null | undefined): value is Record<PropertyKey, unknown>;
257
406
 
258
407
  /**
259
408
  * This file is part of helpers4.
@@ -268,7 +417,8 @@ declare function isNonEmpty(value: Record<PropertyKey, unknown>): boolean;
268
417
  * when `mapKey` is omitted the original keys are preserved.
269
418
  *
270
419
  * Note: if two different keys map to the same output key the last one wins
271
- * (insertion order).
420
+ * (insertion order). Entries whose mapped key is a prototype-polluting string
421
+ * (`__proto__`, `constructor`, `prototype`) are silently skipped.
272
422
  *
273
423
  * @param obj - The source object
274
424
  * @param mapValue - Callback called with `(value, key)` for each entry.
@@ -287,7 +437,69 @@ declare function isNonEmpty(value: Record<PropertyKey, unknown>): boolean;
287
437
  * // => { A: 10, B: 20 }
288
438
  * @since 2.0.0
289
439
  */
290
- 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>;
440
+ 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>;
441
+
442
+ /**
443
+ * This file is part of helpers4.
444
+ * Copyright (C) 2025 baxyz
445
+ * SPDX-License-Identifier: LGPL-3.0-or-later
446
+ */
447
+
448
+ /**
449
+ * Merges two or more objects deeply, returning a **new** object without mutating any input.
450
+ *
451
+ * Sources are applied left to right. For each key:
452
+ * - If both sides are plain objects → merged recursively into a new object.
453
+ * - Otherwise → **later source wins** (the earlier value is overwritten).
454
+ * - `undefined` in a source **never overwrites** an existing value.
455
+ * - `null` in a source **does overwrite** and prevents a later source from
456
+ * deep-merging into that key: `mergeDeep({ a: { x: 1 } }, { a: null }, { a: { y: 2 } })`
457
+ * → `{ a: { y: 2 } }` — `x: 1` is permanently lost.
458
+ * - Arrays, class instances, and all non-plain-object values are **replaced**, not merged.
459
+ *
460
+ * Own enumerable string and symbol keys of each source are processed at the top level;
461
+ * inherited and non-enumerable properties are skipped. Prototype-polluting keys
462
+ * (`__proto__`, `constructor`, `prototype`) are silently ignored.
463
+ * **Note:** symbol keys inside nested plain-object values are not preserved — they are
464
+ * lost when those values are deep-cloned. Only top-level symbol keys survive.
465
+ *
466
+ * **TypeScript return type — intersection semantics**
467
+ *
468
+ * The return type is `A & B & C …` (intersection of all source types). This is accurate
469
+ * when keys are disjoint or share the same type:
470
+ * ```ts
471
+ * mergeDeep({ a: 1 }, { b: 'x' }) // { a: number } & { b: string } ✓
472
+ * mergeDeep({ a: { b: 1 } }, { a: { c: 2 } }) // { a: { b: number } & { c: number } } ✓
473
+ * ```
474
+ * When the same key carries **incompatible types** across sources, the intersection
475
+ * resolves to `never` for that key — TypeScript surfaces the conflict rather than
476
+ * silently picking a type:
477
+ * ```ts
478
+ * mergeDeep({ a: 1 }, { a: 'x' }) // { a: never } ← type conflict detected
479
+ * ```
480
+ * At runtime the later source always wins (`'x'`), but the `never` type signals that
481
+ * the caller should align their source types. If intentional, cast the result.
482
+ *
483
+ * @param sources - Two or more objects to merge (none are mutated)
484
+ * @returns A new object that is the deep merge of all sources
485
+ * @example
486
+ * mergeDeep({ a: 1, b: { c: 2 } }, { b: { d: 3 }, e: 4 })
487
+ * // => { a: 1, b: { c: 2, d: 3 }, e: 4 }
488
+ *
489
+ * // Later source wins for non-object values
490
+ * mergeDeep({ a: 1, arr: [1, 2] }, { a: 2, arr: [3] })
491
+ * // => { a: 2, arr: [3] }
492
+ *
493
+ * // undefined never overwrites
494
+ * mergeDeep({ a: 1 }, { a: undefined })
495
+ * // => { a: 1 }
496
+ *
497
+ * // Three sources, left to right
498
+ * mergeDeep({ x: 1 }, { y: 2 }, { x: 99, z: 3 })
499
+ * // => { x: 99, y: 2, z: 3 }
500
+ * @since 1.9.0
501
+ */
502
+ declare function mergeDeep<T extends [object, ...object[]]>(...sources: [...T]): MergeResult<T>;
291
503
 
292
504
  /**
293
505
  * This file is part of helpers4.
@@ -385,15 +597,48 @@ declare function safeJsonParse<T>(input: string, fallback: T): T;
385
597
  * Copyright (C) 2025 baxyz
386
598
  * SPDX-License-Identifier: LGPL-3.0-or-later
387
599
  */
600
+
388
601
  /**
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
602
+ * Sets a value in an object at the given path, creating intermediate objects as needed.
603
+ *
604
+ * **Two path forms are supported:**
605
+ *
606
+ * 1. **String path** — dot notation and bracket notation, mixed freely.
607
+ * - Dot segments are always string keys: `'layers.1.name'` → keys `['layers', '1', 'name']`.
608
+ * - Bracket segments are always number keys: `'layers[1].name'` → keys `['layers', 1, 'name']`.
609
+ * - String literal paths give **full compile-time type inference** on the return type.
610
+ * - Dynamic (non-literal) strings return `T` (same object type).
611
+ *
612
+ * 2. **Key array** (`PropertyKey[]`) — explicit array of `string | number | symbol` keys,
613
+ * no parsing performed. Enables symbol-keyed access and precise return-type inference.
614
+ *
615
+ * Both forms support **all objects** (plain objects, arrays, class instances).
616
+ * Symbol keys are only reachable via the key-array form.
617
+ *
618
+ * Intermediate nodes that are absent, `null`, or not an object are replaced with `{}`.
619
+ * Any path containing a string segment equal to `__proto__`, `constructor`, or `prototype`
620
+ * is rejected and the original object is returned unchanged (prototype-pollution guard).
621
+ *
622
+ * @param obj - The object to mutate
623
+ * @param path - Dot/bracket-notation string literal or explicit `PropertyKey[]`
624
+ * @param value - Value to assign at the path
625
+ * @returns The mutated object (same reference, narrowed type)
626
+ * @example
627
+ * // Dot notation — "1" stays a string key
628
+ * set({}, 'a.b.c', 42)
629
+ * // => { a: { b: { c: 42 } } }
630
+ *
631
+ * // Bracket notation — [1] becomes a number key
632
+ * set({ layers: [{}, { name: 'old' }] }, 'layers[1].name', 'bg')
633
+ * // => { layers: [{}, { name: 'bg' }] }
634
+ *
635
+ * // Key array — supports symbols
636
+ * const id = Symbol('id')
637
+ * set({}, ['user', id], 'alice')
638
+ * // => { user: { [id]: 'alice' } }
394
639
  * @since 1.9.0
395
640
  */
396
- declare function set(obj: Record<string, unknown>, path: string, value: unknown): Record<string, unknown>;
641
+ 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>;
397
642
 
398
- export { compact, deepClone, deepMerge, diff, equalsDeep, equalsShallow, get, groupBy, invert, isEmpty, isNonEmpty, map, omit, pick, removeUndefinedNull, safeJsonParse, set };
643
+ export { cloneDeep, compact, cloneDeep as deepClone, mergeDeep as deepMerge, diff, equalsDeep, equalsShallow, get, groupBy, invert, isEmpty, isNonEmpty, map, mergeDeep, omit, pick, removeUndefinedNull, safeJsonParse, set };
399
644
  export type { DiffOptions, DiffResult };