# @helpers4/object

> Tree-shakable TypeScript utility functions for the `object` domain.
> Package: `@helpers4/object` — Version: 3.0.2
> License: LGPL-3.0-or-later

## Installation

```sh
npm install @helpers4/object
# or
pnpm add @helpers4/object
```

## Usage

```typescript
import { clone, cloneDeep, compact, ... } from '@helpers4/object';
```

## Functions

| Function | Description |
|---|---|
| `clone` | Creates a shallow copy of a value — one level deep, unlike cloneDeep.  Unlike a plain `{ ...value }` |
| `cloneDeep` | Creates a deep copy of an object or array.  Recursively clones own enumerable string keys. `Date` in |
| `compact` | Removes all entries with falsy values (`false`, `null`, `undefined`, `0`, `""`, `NaN`) from an objec |
| `diff` | Structural object diff.  Returns `true` when both inputs are deeply equal, otherwise a DiffResult de |
| `equalsDeep` | Recursive structural object equality.  Boolean wrapper around diff \u2014 returns `true` when the tw |
| `equalsShallow` | One-level (shallow) object equality.  Two objects are equal when they share the exact same set of ow |
| `flatten` | Flattens a nested object into a single-level object whose keys are the dot-notation path to each lea |
| `get` | Gets a value from an object using a dot/bracket-notated path or explicit key array.  **Two path form |
| `groupBy` | Groups an array of items by a key derived from each item.  A thin, typed wrapper around `Object.grou |
| `invert` | Returns a new object with keys and values swapped. If multiple keys share the same value, the last o |
| `isEmpty` | Checks if a plain object has no own enumerable string-keyed properties. `null` and `undefined` are t |
| `isNonEmpty` | Checks if a plain object has at least one own enumerable string-keyed property. `null` and `undefine |
| `map` | Transforms the values and/or keys of a plain object in a single pass.  Both callbacks are optional a |
| `mergeDeep` | Merges two or more objects deeply, returning a **new** object without mutating any input.  Sources a |
| `omit` | Creates a new object without the specified keys. |
| `omitBy` | Creates a new object without the own enumerable entries for which `predicate` returns `true`.  Compl |
| `parsePropertyPath` | Parses a dot/bracket-notation property path into an array of string/number key segments — the same n |
| `pick` | Creates a new object with only the specified keys. Keys that are prototype-polluting strings (`__pro |
| `pickBy` | Creates a new object with only the own enumerable entries for which `predicate` returns `true`.  Com |
| `removeUndefinedNull` | Remove null and undefined values from an object. |
| `safeJsonParse` | Parses a JSON string, returning `null` (or a fallback) on any parse failure.  Unlike `JSON.parse`, t |
| `set` | Sets a value in an object at the given path, creating intermediate objects as needed.  **Two path fo |
| `unflatten` | Rebuilds a nested object from a single-level object whose keys are dot-notation paths. The inverse o |
| `unset` | Removes the value at a dot/bracket-notation path or explicit key array, mutating the object in place |
| `update` | Updates the value at a path by applying a function to its current value, creating intermediate objec |

---

## API Reference

### `clone`

Creates a shallow copy of a value — one level deep, unlike cloneDeep.

Unlike a plain `{ ...value }` spread, this correctly reconstructs `Date`,
`Map`, `Set`, and arrays instead of producing an empty (or wrong-shaped)
plain object for them. Primitives are returned as-is. Any other object
(including class instances not listed above) has its own enumerable string
keys shallow-copied into a plain object — the same fallback `cloneDeep`
uses, so the two stay consistent for types neither one special-cases.

```typescript
import { clone } from '@helpers4/object';

clone<T>(value: T): T
```

**Parameters:**

- `value: T` — The value to shallow-clone

**Returns:** `T` — A shallow copy of `value`

**Examples:**

*Shallow-copy a plain object*

Top-level keys are copied; nested values still share references (shallow).

```typescript
const obj = { a: 1, b: { c: 2 } };
const copy = clone(obj);
copy.b === obj.b // => true (same nested reference)
```

*Correctly clones Date/Map/Set, unlike a spread*

{ ...new Date() } produces {} — clone() doesn't have that problem.

```typescript
clone(new Map([['a', 1]]))
// => new Map with the same entries, not {}
```

---

### `cloneDeep`

Creates a deep copy of an object or array.

Recursively clones own enumerable string keys. `Date` instances are
reconstructed with the same epoch value. Prototype-polluting keys
(`__proto__`, `constructor`, `prototype`) are silently skipped.
Does **not** handle circular references.

**Limitation:** symbol-keyed own properties are **not** copied — only string
keys are processed. Use `structuredClone` if symbol propagation is required.

```typescript
import { cloneDeep } from '@helpers4/object';

cloneDeep<T>(obj: T): T
```

**Parameters:**

- `obj: T` — The value to clone

**Returns:** `T` — A deep copy of `obj`

**Examples:**

*Clone a nested object*

Creates a deep copy — modifying the clone does not affect the original.

```typescript
const original = { a: { b: 1 } };
const cloned = cloneDeep(original);
cloned.a.b = 2;
// original.a.b is still 1
```

---

### `compact`

Removes all entries with falsy values (`false`, `null`, `undefined`, `0`, `""`, `NaN`) from an object.
Own keys that are prototype-polluting strings (`__proto__`, `constructor`, `prototype`) are
silently skipped.

```typescript
import { compact } from '@helpers4/object';

compact<T extends Record<string, unknown>>(obj: T): Partial<T>
```

**Parameters:**

- `obj: T` — The source object

**Returns:** `Partial<T>` — A new object containing only entries with truthy values

```typescript
import { compact } from '@helpers4/object';

compact(obj: undefined): undefined
```

**Parameters:**

- `obj: undefined` — The source object

**Returns:** `undefined` — A new object containing only entries with truthy values

```typescript
import { compact } from '@helpers4/object';

compact(obj: null): null
```

**Parameters:**

- `obj: null` — The source object

**Returns:** `null` — A new object containing only entries with truthy values

**Examples:**

*Remove falsy values from object*

Removes all entries with falsy values (false, null, undefined, 0, "", NaN).

```typescript
compact({ a: 1, b: null, c: '', d: 0, e: 'hello' })
// => { a: 1, e: 'hello' }
```

*Clean up API response*

Useful to strip empty or missing fields before sending data.

```typescript
compact({ name: 'Alice', email: '', age: 0, role: 'admin' })
// => { name: 'Alice', role: 'admin' }
```

---

### `diff`

Structural object diff.

Returns `true` when both inputs are deeply equal, otherwise a
DiffResult describing the differences key by key.

Comparison rules:
- Same reference \u2192 `true`.
- Either side is `null`/`undefined` (and not both) \u2192 `false`.
- Both `Date` \u2192 epoch comparison.
- Both arrays \u2192 compared with `array/equalsDeep` (leaf, no diff drill-down).
- Special objects (Map, Set, RegExp, Promise, class instances\u2026) \u2192 reference equality.
- Plain objects \u2192 key-by-key, recursing up to `options.depth` levels.
- Mixed types (e.g. array vs object, Date vs object) \u2192 `false`.

For a boolean wrapper see equalsDeep from this category.
For a one-level boolean check see equalsShallow from this category.

```typescript
import { diff } from '@helpers4/object';

diff(objA: object | null | undefined, objB: object | null | undefined, options: DiffOptions): boolean | DiffResult
```

**Parameters:**

- `objA: object | null | undefined` — First value (object, `null`, or `undefined`).
- `objB: object | null | undefined` — Second value (object, `null`, or `undefined`).
- `options: DiffOptions` (default: `{}`) — See DiffOptions.

**Returns:** `boolean | DiffResult` — `true` when equal, otherwise a DiffResult, or `false` for incompatible types.

**Examples:**

*Compare nested objects*

Deeply compares two objects, returning true when they are structurally equal.

```typescript
diff({ a: { b: 1 } }, { a: { b: 1 } })
// => true
```

*Detect deep differences*

Returns a detailed diff object when nested values differ.

```typescript
diff({ a: { b: 1 } }, { a: { b: 2 } })
// => { a: { b: false } }
```

---

### `equalsDeep`

Recursive structural object equality.

Boolean wrapper around diff \u2014 returns `true` when the two values
are deeply equal according to the same rules. Use this when you only
need a yes/no answer; use diff when you also need to know
*what* differs.

For a one-level boolean check use equalsShallow.

```typescript
import { equalsDeep } from '@helpers4/object';

equalsDeep(objA: object | null | undefined, objB: object | null | undefined): boolean
```

**Parameters:**

- `objA: object | null | undefined` — First value (object, `null`, or `undefined`).
- `objB: object | null | undefined` — Second value (object, `null`, or `undefined`).

**Returns:** `boolean` — `true` if both inputs are deeply equal, `false` otherwise.

**Examples:**

*Compare nested objects*

Recursive structural equality. Returns true when the two values are deeply equal.

```typescript
equalsDeep({ a: { b: 1 } }, { a: { b: 1 } })
// => true
```

*Detect deep differences*

Returns false when nested values differ.

```typescript
equalsDeep({ a: { b: 1 } }, { a: { b: 2 } })
// => false
```

---

### `equalsShallow`

One-level (shallow) object equality.

Two objects are equal when they share the exact same set of own
enumerable string keys and each pair of values satisfies strict equality
(`===`). No recursion: nested objects/arrays are compared by reference.

Falls back to strict equality when either input is `null`, `undefined`
or not an object \u2014 so primitives match if and only if they are `===`.
Arrays are not supported; they always return `false` (unless identical
references). Use `array/equalsShallow` instead.

For recursive structural comparison use equalsDeep. For a diff
structure use diff.

```typescript
import { equalsShallow } from '@helpers4/object';

equalsShallow(objA: unknown, objB: unknown): boolean
```

**Parameters:**

- `objA: unknown` — First value to compare
- `objB: unknown` — Second value to compare

**Returns:** `boolean` — `true` if values are shallowly equal, `false` otherwise.

**Examples:**

*Compare two equal objects*

Uses JSON.stringify for a fast comparison.

```typescript
equalsShallow({ a: 1, b: 2 }, { a: 1, b: 2 })
// => true
```

---

### `flatten`

Flattens a nested object into a single-level object whose keys are the
dot-notation path to each leaf value. The inverse of unflatten.

Only **plain objects** are recursed into — arrays, `Date`, `Map`, `RegExp`,
class instances, and empty plain objects `{}` are kept as opaque leaf
values. This keeps `flatten`/`unflatten` a clean, invertible pair: arrays
can't be losslessly told apart from plain objects once reduced to dotted
keys, so this implementation doesn't attempt it.

Caveat shared by every dotted-path flattening scheme: a key that itself
contains a literal `.` is indistinguishable from real nesting once
flattened (`{ 'a.b': 1 }` and `{ a: { b: 1 } }` both produce `{ 'a.b': 1 }`).

```typescript
import { flatten } from '@helpers4/object';

flatten(obj: Record<string, unknown>): Record<string, unknown>
```

**Parameters:**

- `obj: Record<string, unknown>` — The object to flatten

**Returns:** `Record<string, unknown>` — A single-level object with dot-notation keys

**Examples:**

*Flatten a nested config object*

Each key in the result is the full dot-notation path to a leaf value.

```typescript
flatten({ server: { host: 'localhost', port: 8080 } })
// => { 'server.host': 'localhost', 'server.port': 8080 }
```

*Arrays and special objects are kept as leaves*

Only plain objects are recursed into — arrays stay intact.

```typescript
flatten({ tags: ['a', 'b'], meta: { owner: 'x' } })
// => { tags: ['a', 'b'], 'meta.owner': 'x' }
```

---

### `get`

Gets a value from an object using a dot/bracket-notated path or explicit key array.

**Two path forms are supported:**

1. **String path** — dot notation (`'a.b.c'`) and bracket notation (`'layers[1].name'`)
   are both accepted and mixed freely.
   - Dot segments are always string keys: `'a.1'` → key `'1'` (string).
   - Bracket segments are always number keys: `'a[1]'` → key `1` (number).
   - String literal paths give **full compile-time type inference** on the return type.
   - Dynamic (non-literal) strings return `unknown`.

2. **Key array** (`PropertyKey[]`) — explicit array of `string | number | symbol` keys,
   no parsing performed. Enables symbol-keyed traversal and compile-time type inference:
   `get(obj, ['a', 'b'] as const)` infers the return type from the path.

Both forms support **all objects** (plain objects, arrays, class instances).
Symbol keys are only reachable via the key-array form.

```typescript
import { get } from '@helpers4/object';

get<D>(obj: null | undefined, path: string | readonly PropertyKey[], defaultValue: D): D
```

**Parameters:**

- `obj: null | undefined` — The object to read from
- `path: string | readonly PropertyKey[]` — Dot/bracket-notation string literal or explicit `PropertyKey[]`
- `defaultValue: D` — Returned when the path is absent or resolves to `undefined`

**Returns:** `D` — The value at the path, or `defaultValue`

```typescript
import { get } from '@helpers4/object';

get(obj: null | undefined, path: string | readonly PropertyKey[]): undefined
```

**Parameters:**

- `obj: null | undefined` — The object to read from
- `path: string | readonly PropertyKey[]` — Dot/bracket-notation string literal or explicit `PropertyKey[]`

**Returns:** `undefined` — The value at the path, or `defaultValue`

```typescript
import { get } from '@helpers4/object';

get<T extends object, P extends string | readonly PropertyKey[]>(obj: T | null | undefined, path: P, defaultValue?: DeepGet<T, ParsePath<P>>): DeepGet<T, ParsePath<P>> | undefined
```

**Parameters:**

- `obj: T | null | undefined` — The object to read from
- `path: P` — Dot/bracket-notation string literal or explicit `PropertyKey[]`
- `defaultValue?: DeepGet<T, ParsePath<P>>` — Returned when the path is absent or resolves to `undefined`

**Returns:** `DeepGet<T, ParsePath<P>> | undefined` — The value at the path, or `defaultValue`

**Examples:**

*Access a nested property*

Uses a dot-notated path to retrieve a deeply nested value.

```typescript
get({ a: { b: { c: 42 } } }, 'a.b.c')
// => 42
```

*Return default for missing path*

Returns the default value when the path does not exist.

```typescript
get({ a: 1 }, 'b.c', 'default')
// => 'default'
```

*Get via key array (supports symbols)*

Pass an explicit PropertyKey[] to bypass parsing. Supports string, number, and symbol keys.

```typescript
const id = Symbol('id')
get({ [id]: 'alice' }, [id])
// => 'alice'
```

---

### `groupBy`

Groups an array of items by a key derived from each item.

A thin, typed wrapper around `Object.groupBy` (ES2024) that works on
older targets and provides stricter return-type inference.
`null` and `undefined` are treated as empty arrays and return `{}`.
Items whose computed key is a prototype-polluting string (`__proto__`,
`constructor`, `prototype`) are silently skipped.

```typescript
import { groupBy } from '@helpers4/object';

groupBy<T, K extends PropertyKey>(items: readonly T[] | null | undefined, keyFn: function): Partial<Record<K, T[]>>
```

**Parameters:**

- `items: readonly T[] | null | undefined` — The array to group
- `keyFn: function` — Function that returns the group key for each item

**Returns:** `Partial<Record<K, T[]>>` — A record mapping each key to the array of items with that key

**Examples:**

*Group numbers by parity*

Groups elements by the string key returned by the callback.

```typescript
groupBy([1, 2, 3, 4], n => n % 2 === 0 ? 'even' : 'odd')
// => { odd: [1, 3], even: [2, 4] }
```

*Group objects by a property*

Use a property accessor as the grouping key.

```typescript
const users = [
  { name: 'Alice', role: 'admin' },
  { name: 'Bob',   role: 'user'  },
  { name: 'Carol', role: 'admin' },
];
groupBy(users, u => u.role)
// => { admin: [{...Alice}, {...Carol}], user: [{...Bob}] }
```

---

### `invert`

Returns a new object with keys and values swapped.
If multiple keys share the same value, the last one wins.
`null` and `undefined` are treated as empty objects and return `{}`.
Entries whose source key **or** value is a prototype-polluting string
(`__proto__`, `constructor`, `prototype`) are silently skipped.

```typescript
import { invert } from '@helpers4/object';

invert<K extends string, V extends PropertyKey>(obj: Record<K, V> | null | undefined): Record<V, K>
```

**Parameters:**

- `obj: Record<K, V> | null | undefined` — The object whose keys and values are to be swapped

**Returns:** `Record<V, K>` — A new object with values as keys and original keys as values

**Examples:**

*Swap keys and values*

Returns a new object with keys and values swapped.

```typescript
invert({ a: 'x', b: 'y', c: 'z' })
// => { x: 'a', y: 'b', z: 'c' }
```

*Build a reverse lookup map*

Useful when you have a code-to-label map and need label-to-code.

```typescript
const STATUS_LABELS = { 200: 'OK', 404: 'Not Found', 500: 'Internal Server Error' };
const LABEL_TO_CODE = invert(STATUS_LABELS);

LABEL_TO_CODE['OK']; // => '200'
```

---

### `isEmpty`

Checks if a plain object has no own enumerable string-keyed properties.
`null` and `undefined` are treated as empty objects and return `true`.

Symbol-keyed properties are not counted. Use `Object.getOwnPropertySymbols`
separately if symbol keys matter for your use case.

```typescript
import { isEmpty } from '@helpers4/object';

isEmpty(value: Record<PropertyKey, unknown> | null | undefined): boolean
```

**Parameters:**

- `value: Record<PropertyKey, unknown> | null | undefined` — The object to check

**Returns:** `boolean` — `true` if the object has no own enumerable string-keyed properties, or if `value` is `null`/`undefined`

**Examples:**

*Check if an object has no own string-keyed properties*

Returns true for `{}`. Symbol-keyed properties are not counted.

```typescript
isEmpty({})              // => true
isEmpty({ a: 1 })        // => false
isEmpty({ a: undefined }) // => false  (key exists even if value is undefined)
```

*Symbol keys are not counted*

An object with only symbol-keyed properties is considered empty.

```typescript
const sym = Symbol('x');
const obj = { [sym]: 1 };
isEmpty(obj) // => true  (only string keys are counted)
```

---

### `isNonEmpty`

Checks if a plain object has at least one own enumerable string-keyed property.
`null` and `undefined` are treated as empty objects and return `false`.

Symbol-keyed properties are not counted. Use `Object.getOwnPropertySymbols`
separately if symbol keys matter for your use case.

```typescript
import { isNonEmpty } from '@helpers4/object';

isNonEmpty(value: Record<PropertyKey, unknown> | null | undefined): value is Record<PropertyKey, unknown>
```

**Parameters:**

- `value: Record<PropertyKey, unknown> | null | undefined` — The object to check

**Returns:** `value is Record<PropertyKey, unknown>` — `true` if the object has at least one own enumerable string-keyed property; `false` for empty, `null`, or `undefined`.
Acts as a type guard: the `if` branch narrows `Record<PropertyKey, unknown> | null | undefined` to `Record<PropertyKey, unknown>`.

**Examples:**

*Check if an object has own string-keyed properties*

Returns true when at least one own enumerable string key is present.

```typescript
isNonEmpty({ a: 1 })        // => true
isNonEmpty({ a: undefined }) // => true  (key exists)
isNonEmpty({})              // => false
```

*Guard before iterating object keys*

Use isNonEmpty before looping to avoid processing empty objects.

```typescript
function processConfig(config: Record<string, unknown>): void {
  if (!isNonEmpty(config)) {
    console.warn('Config is empty');
    return;
  }
  for (const key of Object.keys(config)) {
    // process each key
  }
}
```

---

### `map`

Transforms the values and/or keys of a plain object in a single pass.

Both callbacks are optional and default to identity (no transformation).
When `mapValue` is omitted the original values are preserved;
when `mapKey` is omitted the original keys are preserved.

Note: if two different keys map to the same output key the last one wins
(insertion order). Entries whose mapped key is a prototype-polluting string
(`__proto__`, `constructor`, `prototype`) are silently skipped.

```typescript
import { map } from '@helpers4/object';

map<TObj extends Record<string, unknown>, TVal = indexedAccess, TKey extends PropertyKey = keyof TObj>(obj: TObj | null | undefined, mapValue?: function, mapKey?: function): Record<TKey, TVal>
```

**Parameters:**

- `obj: TObj | null | undefined` — The source object
- `mapValue?: function` — Callback called with `(value, key)` for each entry.
  Defaults to identity.
- `mapKey?: function` — Callback called with `(key, value)` for each entry.
  Defaults to identity.

**Returns:** `Record<TKey, TVal>` — A new object with transformed keys and/or values

**Examples:**

*Transform values*

Maps each value of an object through a transform function.

```typescript
map({ a: 1, b: 2 }, v => v * 10)
// => { a: 10, b: 20 }
```

*Transform keys*

Maps each key of an object through a transform function.

```typescript
map({ a: 1, b: 2 }, undefined, k => k.toUpperCase())
// => { A: 1, B: 2 }
```

*Transform both keys and values in a single pass*

Provide both a value mapper and a key mapper to rewrite the whole object.

```typescript
map(
  { price: 100, discount: 20 },
  v => v / 100,
  k => `${k}Ratio`
)
// => { priceRatio: 1, discountRatio: 0.2 }
```

---

### `mergeDeep`

Merges two or more objects deeply, returning a **new** object without mutating any input.

Sources are applied left to right. For each key:
- If both sides are plain objects → merged recursively into a new object.
- Otherwise → **later source wins** (the earlier value is overwritten).
- `undefined` in a source **never overwrites** an existing value.
- `null` in a source **does overwrite** and prevents a later source from
  deep-merging into that key: `mergeDeep({ a: { x: 1 } }, { a: null }, { a: { y: 2 } })`
  → `{ a: { y: 2 } }` — `x: 1` is permanently lost.
- Arrays, class instances, and all non-plain-object values are **replaced**, not merged.

Own enumerable string and symbol keys of each source are processed at the top level;
inherited and non-enumerable properties are skipped. Prototype-polluting keys
(`__proto__`, `constructor`, `prototype`) are silently ignored.
**Note:** symbol keys inside nested plain-object values are not preserved — they are
lost when those values are deep-cloned. Only top-level symbol keys survive.

**TypeScript return type — intersection semantics**

The return type is `A & B & C …` (intersection of all source types). This is accurate
when keys are disjoint or share the same type:
```ts
mergeDeep({ a: 1 }, { b: 'x' })           // { a: number } & { b: string }  ✓
mergeDeep({ a: { b: 1 } }, { a: { c: 2 } }) // { a: { b: number } & { c: number } }  ✓
```
When the same key carries **incompatible types** across sources, the intersection
resolves to `never` for that key — TypeScript surfaces the conflict rather than
silently picking a type:
```ts
mergeDeep({ a: 1 }, { a: 'x' })  // { a: never }  ← type conflict detected
```
At runtime the later source always wins (`'x'`), but the `never` type signals that
the caller should align their source types. If intentional, cast the result.

```typescript
import { mergeDeep } from '@helpers4/object';

mergeDeep<T extends [object, rest]>(sources: [rest]): MergeResult<T>
```

**Parameters:**

- `sources: [rest]` — Two or more objects to merge (none are mutated)

**Returns:** `MergeResult<T>` — A new object that is the deep merge of all sources

**Examples:**

*Merge two objects deeply*

Returns a new object — neither input is mutated.

```typescript
mergeDeep({ a: 1, b: { c: 2 } }, { b: { d: 3 }, e: 4 })
// => { a: 1, b: { c: 2, d: 3 }, e: 4 }
```

---

### `omit`

Creates a new object without the specified keys.

```typescript
import { omit } from '@helpers4/object';

omit<T extends Record<string, unknown>, K extends string | number | symbol>(obj: T, keys: readonly K[]): Omit<T, K>
```

**Parameters:**

- `obj: T` — The source object
- `keys: readonly K[]` — The keys to omit

**Returns:** `Omit<T, K>` — A new object without the omitted keys

```typescript
import { omit } from '@helpers4/object';

omit(obj: undefined, keys: readonly string[]): undefined
```

**Parameters:**

- `obj: undefined` — The source object
- `keys: readonly string[]` — The keys to omit

**Returns:** `undefined` — A new object without the omitted keys

```typescript
import { omit } from '@helpers4/object';

omit(obj: null, keys: readonly string[]): null
```

**Parameters:**

- `obj: null` — The source object
- `keys: readonly string[]` — The keys to omit

**Returns:** `null` — A new object without the omitted keys

**Examples:**

*Omit specific keys*

Creates a new object without the specified keys.

```typescript
omit({ a: 1, b: 2, c: 3 }, ['b'])
// => { a: 1, c: 3 }
```

*Remove sensitive fields*

Useful to strip sensitive data before sending to client.

```typescript
const user = { id: 1, name: 'Alice', password: 'secret', token: 'abc123' };
omit(user, ['password', 'token'])
// => { id: 1, name: 'Alice' }
```

---

### `omitBy`

Creates a new object without the own enumerable entries for which
`predicate` returns `true`.

Complements omit for when the keys to remove aren't known ahead of
time — `omit` takes an explicit key list, `omitBy` takes a predicate.

```typescript
import { omitBy } from '@helpers4/object';

omitBy<T extends Record<string, unknown>>(obj: T | null | undefined, predicate: function): Partial<T> | null | undefined
```

**Parameters:**

- `obj: T | null | undefined` — The source object. `null`/`undefined` pass through unchanged.
- `predicate: function` — Called with `(value, key)` for each own enumerable entry

**Returns:** `Partial<T> | null | undefined` — A new object without the matching entries

**Examples:**

*Drop entries matching a predicate*

Unlike omit(), the keys to drop don’t need to be known ahead of time.

```typescript
omitBy({ a: 1, b: undefined, c: 2 }, (value) => value === undefined)
// => { a: 1, c: 2 }
```

*Drop entries by key name*

The predicate also receives the key, so filtering by name works too.

```typescript
omitBy({ id: 1, name: 'x', _internal: true }, (_v, key) => key.startsWith('_'))
// => { id: 1, name: 'x' }
```

---

### `parsePropertyPath`

Parses a dot/bracket-notation property path into an array of string/number
key segments — the same notation accepted by get and set.

- Dot separators (`.`) split segments; each segment becomes a string key.
- Bracket indices (`[n]`) become number keys.
- A leading `.` is treated as "current level" and stripped before parsing,
  so `.[0]` ≡ `[0]` and `.a.b` ≡ `a.b`.
- Empty string (or a bare `.`) returns `['']` (addresses the `''` key on the root object).
- Consecutive dots (`a..b`) produce an empty-string segment: `['a', '', 'b']`.

Results are cached (up to 500 distinct path strings, oldest evicted first)
since real-world callers tend to reuse a small, fixed set of literal paths.

```typescript
import { parsePropertyPath } from '@helpers4/object';

parsePropertyPath(path: string): readonly string | number[]
```

**Parameters:**

- `path: string` — The dot/bracket-notation path string to parse

**Returns:** `readonly string | number[]` — The parsed key segments

**Examples:**

*Parse a dot/bracket-notation path into key segments*

The same notation accepted by get() and set() — bracket indices become numbers.

```typescript
parsePropertyPath('layers[1].name')
// => ['layers', 1, 'name']
```

*Malformed paths throw instead of silently misparsing*

Text trailing a closing bracket within a segment is ambiguous — use 'a[0].b' instead.

```typescript
parsePropertyPath('a[0]b')
// => throws RangeError
```

---

### `pick`

Creates a new object with only the specified keys.
Keys that are prototype-polluting strings (`__proto__`, `constructor`, `prototype`) are
silently skipped.

```typescript
import { pick } from '@helpers4/object';

pick<T extends Record<string, unknown>, K extends string | number | symbol>(obj: T, keys: readonly K[]): Pick<T, K>
```

**Parameters:**

- `obj: T` — The source object
- `keys: readonly K[]` — The keys to pick

**Returns:** `Pick<T, K>` — A new object with only the picked keys

```typescript
import { pick } from '@helpers4/object';

pick(obj: undefined, keys: readonly string[]): undefined
```

**Parameters:**

- `obj: undefined` — The source object
- `keys: readonly string[]` — The keys to pick

**Returns:** `undefined` — A new object with only the picked keys

```typescript
import { pick } from '@helpers4/object';

pick(obj: null, keys: readonly string[]): null
```

**Parameters:**

- `obj: null` — The source object
- `keys: readonly string[]` — The keys to pick

**Returns:** `null` — A new object with only the picked keys

**Examples:**

*Pick specific keys*

Creates a new object with only the specified keys.

```typescript
pick({ a: 1, b: 2, c: 3 }, ['a', 'c'])
// => { a: 1, c: 3 }
```

*Extract user fields*

Useful to select only the fields you need from an object.

```typescript
const user = { id: 1, name: 'Alice', email: 'alice@example.com', password: 'secret' };
pick(user, ['id', 'name', 'email'])
// => { id: 1, name: 'Alice', email: 'alice@example.com' }
```

---

### `pickBy`

Creates a new object with only the own enumerable entries for which
`predicate` returns `true`.

Complements pick for when the keys to keep aren't known ahead of
time — `pick` takes an explicit key list, `pickBy` takes a predicate.

```typescript
import { pickBy } from '@helpers4/object';

pickBy<T extends Record<string, unknown>>(obj: T | null | undefined, predicate: function): Partial<T> | null | undefined
```

**Parameters:**

- `obj: T | null | undefined` — The source object. `null`/`undefined` pass through unchanged.
- `predicate: function` — Called with `(value, key)` for each own enumerable entry

**Returns:** `Partial<T> | null | undefined` — A new object with only the matching entries

**Examples:**

*Keep only entries matching a predicate*

Unlike pick(), the keys to keep don’t need to be known ahead of time.

```typescript
pickBy({ a: 1, b: 0, c: 2 }, (value) => value > 0)
// => { a: 1, c: 2 }
```

*Keep entries by key name*

The predicate also receives the key, so filtering by name works too.

```typescript
pickBy({ _id: 1, name: 'x', _internal: true }, (_v, key) => !key.startsWith('_'))
// => { name: 'x' }
```

---

### `removeUndefinedNull`

Remove null and undefined values from an object.

```typescript
import { removeUndefinedNull } from '@helpers4/object';

removeUndefinedNull<T extends Record<string, string | number | boolean | null | undefined>>(obj: T): Partial<T>
```

**Parameters:**

- `obj: T` — an object

**Returns:** `Partial<T>` — A shallow copy of the object without null or undefined values

```typescript
import { removeUndefinedNull } from '@helpers4/object';

removeUndefinedNull(obj: null): null
```

**Parameters:**

- `obj: null` — a null object

**Returns:** `null` — null

```typescript
import { removeUndefinedNull } from '@helpers4/object';

removeUndefinedNull(obj: undefined): undefined
```

**Parameters:**

- `obj: undefined` — an undefined object

**Returns:** `undefined` — undefined

**Examples:**

*Strip null and undefined values*

Returns a shallow copy of the object without null or undefined properties.

```typescript
removeUndefinedNull({ a: 1, b: null, c: undefined, d: 'ok' })
// => { a: 1, d: 'ok' }
```

---

### `safeJsonParse`

Parses a JSON string, returning `null` (or a fallback) on any parse failure.

Unlike `JSON.parse`, this never throws. Invalid JSON strings and other
parsing edge-cases resolve to `null` or the provided `fallback`.

```typescript
import { safeJsonParse } from '@helpers4/object';

safeJsonParse<T>(input: string): T | null
```

**Parameters:**

- `input: string` — The JSON string to parse.

**Returns:** `T | null` — The parsed value typed as `T`, or `fallback` on failure.

```typescript
import { safeJsonParse } from '@helpers4/object';

safeJsonParse<T>(input: string, fallback: T): T
```

**Parameters:**

- `input: string` — The JSON string to parse.
- `fallback: T` — Value returned on failure. Defaults to `null` when omitted.

**Returns:** `T` — The parsed value typed as `T`, or `fallback` on failure.

**Examples:**

*Parse valid JSON*

Returns the parsed value when the input is valid JSON.

```typescript
safeJsonParse<{ a: number }>('{"a":1}')
// => { a: 1 }
```

*Return null on invalid input*

Returns null instead of throwing when JSON is malformed.

```typescript
safeJsonParse('invalid')
// => null
```

*Use a fallback value*

Returns the provided fallback when parsing fails.

```typescript
safeJsonParse('invalid', [])
// => []
```

---

### `set`

Sets a value in an object at the given path, creating intermediate objects as needed.

**Two path forms are supported:**

1. **String path** — dot notation and bracket notation, mixed freely.
   - Dot segments are always string keys: `'layers.1.name'` → keys `['layers', '1', 'name']`.
   - Bracket segments are always number keys: `'layers[1].name'` → keys `['layers', 1, 'name']`.
   - String literal paths give **full compile-time type inference** on the return type.
   - Dynamic (non-literal) strings return `T` (same object type).

2. **Key array** (`PropertyKey[]`) — explicit array of `string | number | symbol` keys,
   no parsing performed. Enables symbol-keyed access and precise return-type inference.

Both forms support **all objects** (plain objects, arrays, class instances).
Symbol keys are only reachable via the key-array form.

Intermediate nodes that are absent, `null`, or not an object are replaced with `{}`.
Any path containing a string segment equal to `__proto__`, `constructor`, or `prototype`
is rejected and the original object is returned unchanged (prototype-pollution guard).

```typescript
import { set } from '@helpers4/object';

set<T extends object, P extends string | readonly PropertyKey[], V extends unknown>(obj: T, path: P, value: V): conditional
```

**Parameters:**

- `obj: T` — The object to mutate
- `path: P` — Dot/bracket-notation string literal or explicit `PropertyKey[]`
- `value: V` — Value to assign at the path

**Returns:** `conditional` — The mutated object (same reference, narrowed type)

**Examples:**

*Set a nested property (dot notation)*

Creates intermediate objects as needed. All segments are string keys — including numeric-looking ones like "1".

```typescript
set({}, 'a.b.c', 42)
// => { a: { b: { c: 42 } } }

set({}, 'layers.1.name', 'bg')
// => { layers: { '1': { name: 'bg' } } }   // '1' is a string key
```

*Set via bracket notation*

Square-bracket indices become numeric keys. Useful when the path targets an array element.

```typescript
const obj = { layers: [{}, { name: 'old' }] }
set(obj, 'layers[1].name', 'new')
// => { layers: [{}, { name: 'new' }] }
```

*Set via key array (supports symbols)*

Pass an explicit PropertyKey[] to bypass parsing. Supports string, number, and symbol keys.

```typescript
const id = Symbol('id')
set({}, ['user', id], 'alice')
// => { user: { [id]: 'alice' } }
```

---

### `unflatten`

Rebuilds a nested object from a single-level object whose keys are
dot-notation paths. The inverse of flatten.

Uses set internally, so intermediate nodes are always created as
plain objects (never arrays — see flatten's doc for why), and any
key segment equal to `__proto__`, `constructor`, or `prototype` is silently
rejected (same prototype-pollution guard as `set`).

```typescript
import { unflatten } from '@helpers4/object';

unflatten(obj: Record<string, unknown>): Record<string, unknown>
```

**Parameters:**

- `obj: Record<string, unknown>` — A single-level object with dot-notation keys

**Returns:** `Record<string, unknown>` — The rebuilt nested object

**Examples:**

*Rebuild a nested object from dotted keys*

The inverse of flatten() — useful for parsing flat config sources (env vars, .ini files).

```typescript
unflatten({ 'server.host': 'localhost', 'server.port': 8080 })
// => { server: { host: 'localhost', port: 8080 } }
```

*Multiple keys under the same parent are merged*

Every dotted key contributes to the same nested structure.

```typescript
unflatten({ 'a.x': 1, 'a.y': 2 })
// => { a: { x: 1, y: 2 } }
```

---

### `unset`

Removes the value at a dot/bracket-notation path or explicit key array,
mutating the object in place. Uses the same path syntax as get/set.

A missing intermediate segment is a no-op (nothing to remove), not an error.
As with `set`, any path containing a string segment equal to `__proto__`,
`constructor`, or `prototype` is rejected and the object is returned unchanged.

The removed key stops appearing in `Object.keys`/`for...in` — unlike setting
it to `undefined`, which would keep the key present.

```typescript
import { unset } from '@helpers4/object';

unset<T extends object>(obj: T, path: string | readonly PropertyKey[]): T
```

**Parameters:**

- `obj: T` — The object to mutate
- `path: string | readonly PropertyKey[]` — Dot/bracket-notation string, or explicit `PropertyKey[]`

**Returns:** `T` — The same object reference, with the key removed if it existed

**Examples:**

*Remove a nested value by path*

Uses the same dot/bracket path syntax as get() and set().

```typescript
const config = { server: { host: 'localhost', debug: true } };
unset(config, 'server.debug')
// => { server: { host: 'localhost' } }
```

*Missing paths are a safe no-op*

Unlike set(), unset() never creates intermediate objects — there is nothing to remove.

```typescript
unset({ a: 1 }, 'x.y.z')
// => { a: 1 }  (unchanged)
```

---

### `update`

Updates the value at a path by applying a function to its current value,
creating intermediate objects as needed. Equivalent to
`set(obj, path, updater(get(obj, path)))` in a single call.

Uses the same path syntax and type-inference rules as get and
set — see those for the full behavior (string vs. `PropertyKey[]`
paths, prototype-pollution guarding, etc.).

```typescript
import { update } from '@helpers4/object';

update<T extends object, P extends string | readonly PropertyKey[], V extends unknown>(obj: T, path: P, updater: function): conditional
```

**Parameters:**

- `obj: T` — The object to mutate
- `path: P` — Dot/bracket-notation string literal or explicit `PropertyKey[]`
- `updater: function` — Called with the current value (`undefined` if the path is
  absent); its return value is written back at the path

**Returns:** `conditional` — The mutated object (same reference, narrowed type)

**Examples:**

*Increment a counter in one call*

Equivalent to set(obj, path, updater(get(obj, path))), without repeating the path.

```typescript
const state = { count: 1 };
update(state, 'count', (n) => (n ?? 0) + 1)
// => { count: 2 }
```

*Missing paths create intermediate objects, like set()*

The updater receives undefined when the path does not exist yet.

```typescript
const stats: Record<string, unknown> = {};
update(stats, 'hits.total', (n: number | undefined) => (n ?? 0) + 1)
// => { hits: { total: 1 } }
```

---
