@helpers4/object 2.0.4 → 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,21 +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
- *
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
+ * @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
49
46
  */
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
+ 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;
53
50
 
54
51
  /**
55
52
  * This file is part of helpers4.
@@ -168,11 +165,81 @@ declare function equalsShallow(objA: unknown, objB: unknown): boolean;
168
165
  * Copyright (C) 2025 baxyz
169
166
  * SPDX-License-Identifier: LGPL-3.0-or-later
170
167
  */
168
+ /**
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]>;
171
238
  /**
172
239
  * Resolves the value type at a given path within T.
173
240
  * Returns `unknown` when the path doesn't match the shape of T.
174
241
  *
175
- * @internal
242
+ * @ignore
176
243
  * @example
177
244
  * DeepGet<{ a: { b: number } }, ['a', 'b']> // => number
178
245
  * DeepGet<{ a: { b: number } }, ['a', 'x']> // => unknown
@@ -184,7 +251,7 @@ type DeepGet<T, Path extends readonly PropertyKey[]> = Path extends readonly []
184
251
  * Resolves to `never` when a key in Path is not present in the corresponding level of T,
185
252
  * surfacing invalid paths at the call site rather than silently returning T unchanged.
186
253
  *
187
- * @internal
254
+ * @ignore
188
255
  * @example
189
256
  * DeepSet<{ a: { b: number } }, ['a', 'b'], string>
190
257
  * // => { a: { b: string } }
@@ -205,29 +272,36 @@ type DeepSet<T, Path extends readonly PropertyKey[], V> = Path extends readonly
205
272
  * **Two path forms are supported:**
206
273
  *
207
274
  * 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.
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`.
210
280
  *
211
281
  * 2. **Key array** (`PropertyKey[]`) — explicit array of `string | number | symbol` keys,
212
282
  * no parsing performed. Enables symbol-keyed traversal and compile-time type inference:
213
283
  * `get(obj, ['a', 'b'] as const)` infers the return type from the path.
214
284
  *
285
+ * Both forms support **all objects** (plain objects, arrays, class instances).
286
+ * Symbol keys are only reachable via the key-array form.
287
+ *
215
288
  * @param obj - The object to read from
216
- * @param path - Dot/bracket-notation string or explicit `PropertyKey[]`
289
+ * @param path - Dot/bracket-notation string literal or explicit `PropertyKey[]`
217
290
  * @param defaultValue - Returned when the path is absent or resolves to `undefined`
218
291
  * @returns The value at the path, or `defaultValue`
219
292
  * @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)
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)
223
296
  * get({ a: 1 }, 'b.c', 'default') // => 'default'
224
297
  *
225
298
  * const id = Symbol('id')
226
- * get({ [id]: 'alice' }, [id]) // => 'alice' (symbol key)
299
+ * get({ [id]: 'alice' }, [id]) // => 'alice' (symbol key, inferred: string | undefined)
227
300
  * @since 1.9.0
228
301
  */
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;
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;
231
305
 
232
306
  /**
233
307
  * This file is part of helpers4.
@@ -239,6 +313,9 @@ declare function get<T extends object, const Path extends readonly PropertyKey[]
239
313
  *
240
314
  * A thin, typed wrapper around `Object.groupBy` (ES2024) that works on
241
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.
242
319
  *
243
320
  * @param items - The array to group
244
321
  * @param keyFn - Function that returns the group key for each item
@@ -246,9 +323,11 @@ declare function get<T extends object, const Path extends readonly PropertyKey[]
246
323
  * @example
247
324
  * groupBy([1, 2, 3, 4], n => n % 2 === 0 ? 'even' : 'odd')
248
325
  * // => { odd: [1, 3], even: [2, 4] }
326
+ * groupBy(null, n => n % 2 === 0 ? 'even' : 'odd')
327
+ * // => {}
249
328
  * @since 2.0.0
250
329
  */
251
- 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[]>>;
252
331
 
253
332
  /**
254
333
  * This file is part of helpers4.
@@ -258,6 +337,9 @@ declare function groupBy<T, K extends PropertyKey>(items: readonly T[], keyFn: (
258
337
  /**
259
338
  * Returns a new object with keys and values swapped.
260
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.
261
343
  *
262
344
  * @param obj - The object whose keys and values are to be swapped
263
345
  * @returns A new object with values as keys and original keys as values
@@ -267,9 +349,12 @@ declare function groupBy<T, K extends PropertyKey>(items: readonly T[], keyFn: (
267
349
  *
268
350
  * invert({ one: 1, two: 2 })
269
351
  * // => { 1: 'one', 2: 'two' }
352
+ *
353
+ * invert(null)
354
+ * // => {}
270
355
  * @since 2.0.0
271
356
  */
272
- 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>;
273
358
 
274
359
  /**
275
360
  * This file is part of helpers4.
@@ -278,19 +363,22 @@ declare function invert<K extends string, V extends PropertyKey>(obj: Record<K,
278
363
  */
279
364
  /**
280
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`.
281
367
  *
282
368
  * Symbol-keyed properties are not counted. Use `Object.getOwnPropertySymbols`
283
369
  * separately if symbol keys matter for your use case.
284
370
  *
285
371
  * @param value - The object to check
286
- * @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`
287
373
  * @example
288
374
  * isEmpty({}) // => true
375
+ * isEmpty(null) // => true
376
+ * isEmpty(undefined) // => true
289
377
  * isEmpty({ a: 1 }) // => false
290
378
  * isEmpty({ a: undefined }) // => false (key exists)
291
379
  * @since 2.0.3
292
380
  */
293
- declare function isEmpty(value: Record<PropertyKey, unknown>): boolean;
381
+ declare function isEmpty(value: Record<PropertyKey, unknown> | null | undefined): boolean;
294
382
 
295
383
  /**
296
384
  * This file is part of helpers4.
@@ -299,18 +387,22 @@ declare function isEmpty(value: Record<PropertyKey, unknown>): boolean;
299
387
  */
300
388
  /**
301
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`.
302
391
  *
303
392
  * Symbol-keyed properties are not counted. Use `Object.getOwnPropertySymbols`
304
393
  * separately if symbol keys matter for your use case.
305
394
  *
306
395
  * @param value - The object to check
307
- * @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>`.
308
398
  * @example
309
399
  * isNonEmpty({ a: 1 }) // => true
310
400
  * isNonEmpty({}) // => false
401
+ * isNonEmpty(null) // => false
402
+ * isNonEmpty(undefined) // => false
311
403
  * @since 2.0.3
312
404
  */
313
- declare function isNonEmpty(value: Record<PropertyKey, unknown>): boolean;
405
+ declare function isNonEmpty(value: Record<PropertyKey, unknown> | null | undefined): value is Record<PropertyKey, unknown>;
314
406
 
315
407
  /**
316
408
  * This file is part of helpers4.
@@ -325,7 +417,8 @@ declare function isNonEmpty(value: Record<PropertyKey, unknown>): boolean;
325
417
  * when `mapKey` is omitted the original keys are preserved.
326
418
  *
327
419
  * Note: if two different keys map to the same output key the last one wins
328
- * (insertion order).
420
+ * (insertion order). Entries whose mapped key is a prototype-polluting string
421
+ * (`__proto__`, `constructor`, `prototype`) are silently skipped.
329
422
  *
330
423
  * @param obj - The source object
331
424
  * @param mapValue - Callback called with `(value, key)` for each entry.
@@ -344,7 +437,69 @@ declare function isNonEmpty(value: Record<PropertyKey, unknown>): boolean;
344
437
  * // => { A: 10, B: 20 }
345
438
  * @since 2.0.0
346
439
  */
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>;
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>;
348
503
 
349
504
  /**
350
505
  * This file is part of helpers4.
@@ -446,47 +601,44 @@ declare function safeJsonParse<T>(input: string, fallback: T): T;
446
601
  /**
447
602
  * Sets a value in an object at the given path, creating intermediate objects as needed.
448
603
  *
449
- * **Three path forms are supported:**
604
+ * **Two path forms are supported:**
450
605
  *
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"`).
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).
453
611
  *
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"`.
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.
457
614
  *
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')]`.
615
+ * Both forms support **all objects** (plain objects, arrays, class instances).
616
+ * Symbol keys are only reachable via the key-array form.
461
617
  *
462
618
  * Intermediate nodes that are absent, `null`, or not an object are replaced with `{}`.
463
619
  * Any path containing a string segment equal to `__proto__`, `constructor`, or `prototype`
464
620
  * is rejected and the original object is returned unchanged (prototype-pollution guard).
465
621
  *
466
622
  * @param obj - The object to mutate
467
- * @param path - Dot/bracket-notation string or explicit `PropertyKey[]`
623
+ * @param path - Dot/bracket-notation string literal or explicit `PropertyKey[]`
468
624
  * @param value - Value to assign at the path
469
- * @returns The mutated object (same reference)
625
+ * @returns The mutated object (same reference, narrowed type)
470
626
  * @example
471
- * // 1. Dot notation — "1" stays a string key
627
+ * // Dot notation — "1" stays a string key
472
628
  * set({}, 'a.b.c', 42)
473
629
  * // => { a: { b: { c: 42 } } }
474
630
  *
475
- * set({}, 'layers.1.name', 'bg')
476
- * // => { layers: { '1': { name: 'bg' } } }
477
- *
478
- * // 2. Bracket notation — [1] becomes a number key
631
+ * // Bracket notation — [1] becomes a number key
479
632
  * set({ layers: [{}, { name: 'old' }] }, 'layers[1].name', 'bg')
480
633
  * // => { layers: [{}, { name: 'bg' }] }
481
634
  *
482
- * // 3. Key array — supports symbols
635
+ * // Key array — supports symbols
483
636
  * const id = Symbol('id')
484
637
  * set({}, ['user', id], 'alice')
485
638
  * // => { user: { [id]: 'alice' } }
486
639
  * @since 1.9.0
487
640
  */
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>;
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>;
490
642
 
491
- 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 };
492
644
  export type { DiffOptions, DiffResult };