@absoluteui/helpers/object
Pick, omit, deep get/set/merge/clone/equal, diff and patch, flatten and unflatten, all prototype-pollution safe.
import { ... } from '@absoluteui/helpers/object';How to use
Section titled “How to use”Immutable helpers for plain objects: pick and reshape records, read and write nested paths, merge settings, and compute what changed between two versions of a record. Every function that writes a dynamic key refuses __proto__, constructor, and prototype, so it is safe on parsed JSON. The Helpers and services guide covers the shared conventions.
How do I send only the fields the user changed?
Section titled “How do I send only the fields the user changed?”diff lists the changes between the loaded record and the edited one; keep the changed top-level keys with pickBy for a PATCH body, or store the change list as an audit entry.
import { diff, pickBy } from '@absoluteui/helpers/object';
const loaded = { id: 'E-7', name: 'Asha Rao', dept: 'HR', address: { city: 'Pune', zip: '411001' } };const edited = { ...loaded, dept: 'Finance', address: { city: 'Pune', zip: '411014' } };const changes = diff(loaded, edited);changes.map((c) => c.path.join('.')); // => ['dept', 'address.zip']const changed = new Set(changes.map((c) => c.path[0]));pickBy(edited, (_value, key) => changed.has(key)); // => { dept: 'Finance', address: { city: 'Pune', zip: '411014' } }How do I layer user settings over defaults?
Section titled “How do I layer user settings over defaults?”deepMerge merges nested plain objects into a new object; arrays replace by default (arrays: 'union' or 'concat' to combine) and undefined values do not overwrite.
import { deepMerge } from '@absoluteui/helpers/object';
const defaults = { grid: { pageSize: 25, density: 'comfortable' }, columns: ['name', 'dept'] };const saved: unknown = JSON.parse('{"grid":{"pageSize":50},"columns":["name","manager"]}');const settings = deepMerge(defaults, typeof saved === 'object' && saved !== null ? saved : {});settings; // => { grid: { pageSize: 50, density: 'comfortable' }, columns: ['name', 'manager'] }deepMerge(defaults, { columns: ['email'] }, { arrays: 'union' }).columns; // => ['name', 'dept', 'email']How do I read and write nested fields by path?
Section titled “How do I read and write nested fields by path?”deepGet and deepSet take a dotted path with [index] segments, as used for form field names; deepSet returns a copy and leaves the original untouched.
import { deepGet, deepSet } from '@absoluteui/helpers/object';
const employee = { name: 'Ben', contacts: [{ type: 'mobile', value: '555-0100' }] };deepGet(employee, 'contacts[0].value'); // => '555-0100'deepGet(employee, 'contacts[3].value', '(none)'); // => '(none)'const updated = deepSet(employee, 'contacts[0].value', '555-0199');[employee.contacts[0]?.value, updated.contacts[0]?.value]; // => ['555-0100', '555-0199']How do I turn a nested record into flat form fields and back?
Section titled “How do I turn a nested record into flat form fields and back?”flattenObject joins keys (for flat form names, CSV columns, or query strings) and unflattenObject restores the structure; compactObject drops empty fields before sending.
import { compactObject, flattenObject, unflattenObject } from '@absoluteui/helpers/object';
const flat = flattenObject({ name: 'Chen', address: { city: 'Oslo', zip: '0150' } });flat; // => { name: 'Chen', 'address.city': 'Oslo', 'address.zip': '0150' }unflattenObject(flat); // => { name: 'Chen', address: { city: 'Oslo', zip: '0150' } }compactObject({ name: 'Chen', middleName: '', phone: null }, { remove: 'empty' }); // => { name: 'Chen' }What goes wrong?
Section titled “What goes wrong?”Keys that would reach the prototype throw HLP_UNSAFE_KEY instead of polluting Object.prototype:
import { deepMerge, deepSet, unflattenObject } from '@absoluteui/helpers/object';
deepMerge({}, JSON.parse('{"__proto__":{"isAdmin":true}}')); // => throws HLP_UNSAFE_KEYdeepSet({}, 'constructor.prototype.isAdmin', true); // => throws HLP_UNSAFE_KEYunflattenObject({ '__proto__.isAdmin': true }); // => throws HLP_UNSAFE_KEYAPI reference
Section titled “API reference”Functions
Section titled “Functions”compactObject
Removes nullish (or empty) values.
function compactObject<T extends object>(obj: T, options?: CompactObjectOptions): Partial<T>;Example:
compactObject({ a: 1, b: null, c: undefined }) // => { a: 1 }countValuesBy
Counts values by a derived key (defaults to the value itself).
function countValuesBy<T extends object>(obj: T, fn?: (value: T[keyof T]) => unknown): Record<string, number>;Example:
countValuesBy({ a: 'x', b: 'y', c: 'x' }) // => { x: 2, y: 1 }defaults
Fills keys that are missing or undefined from the sources, in order.
function defaults<T extends object, S extends object>(obj: T, ...sources: readonly S[]): T & Partial<S>;Example:
defaults({ a: 1, b: undefined }, { b: 2, c: 3 }) // => { a: 1, b: 2, c: 3 }filterKeys
Keeps entries whose key passes predicate.
function filterKeys<T extends object>(obj: T, predicate: (key: keyof T & string) => boolean): Partial<T>;Example:
filterKeys({ a: 1, _b: 2 }, (k) => !k.startsWith('_')) // => { a: 1 }filterValues
Keeps entries whose value passes predicate.
function filterValues<T extends object>(obj: T, predicate: (value: T[keyof T]) => boolean): Partial<T>;Example:
filterValues({ a: 1, b: 0 }, Boolean) // => { a: 1 }getOwn
Own property value, or undefined (never reads inherited properties).
function getOwn(obj: unknown, key: PropertyKey): unknown;Example:
getOwn({}, 'toString') // => undefinedinvert
Swaps keys and values (values must be strings, numbers, booleans or bigints). Later keys win.
function invert(obj: Readonly<Record<string, string | number | boolean | bigint>>): Record<string, string>;Example:
invert({ a: 'x', b: 'y' }) // => { x: 'a', y: 'b' }invertBy
Groups keys by a value-derived key.
function invertBy<T extends object>(obj: T, fn?: (value: T[keyof T]) => unknown): Record<string, (keyof T & string)[]>;Example:
invertBy({ a: 1, b: 2, c: 1 }) // => { '1': ['a', 'c'], '2': ['b'] }isShallowEqual
One-level equality with Object.is for arrays and objects (own enumerable string keys).
function isShallowEqual(a: unknown, b: unknown): boolean;Example:
isShallowEqual({ a: 1 }, { a: 1 }) // => truemapEntries
Transforms entries; return [newKey, newValue].
function mapEntries<T extends object, K extends PropertyKey, V>(obj: T, fn: (entry: [keyof T & string, T[keyof T]]) => readonly [K, V]): Record<K, V>;Example:
mapEntries({ a: 1 }, ([k, v]) => [`${k}!`, v + 1]) // => { 'a!': 2 }mapKeys
Renames keys with fn. Later entries win on collisions.
function mapKeys<T extends object, K extends PropertyKey>(obj: T, fn: (value: T[keyof T], key: keyof T & string) => K): Record<K, T[keyof T]>;Example:
mapKeys({ a: 1 }, (_, k) => k.toUpperCase()) // => { A: 1 }mapToObject
Converts a Map with string/number keys to a plain object. Throws HLP_OBJECT_INVALID_KEY
for other key types and HLP_UNSAFE_KEY for __proto__/constructor/prototype.
function mapToObject<V>(map: ReadonlyMap<unknown, V>): Record<string, V>;Example:
mapToObject(new Map([['a', 1]])) // => { a: 1 }mapValues
Transforms values, keeping keys.
function mapValues<T extends object, R>(obj: T, fn: (value: T[keyof T], key: keyof T & string) => R): Record<keyof T & string, R>;Example:
mapValues({ a: 1 }, (v) => v * 2) // => { a: 2 }objectToMap
Own enumerable string-keyed entries as a Map.
function objectToMap<V>(obj: Readonly<Record<string, V>>): Map<string, V>;Example:
objectToMap({ a: 1 }).get('a') // => 1omit
Drops the listed keys.
function omit<T extends object, K extends keyof T>(obj: T, keys: readonly K[]): Omit<T, K>;Example:
omit({ a: 1, b: 2 }, ['a']) // => { b: 2 }omitBy
Drops entries whose value and key pass predicate.
function omitBy<T extends object>(obj: T, predicate: (value: T[keyof T], key: keyof T & string) => boolean): Partial<T>;Example:
omitBy({ a: 1, b: null }, (v) => v === null) // => { a: 1 }pick
Keeps the listed own keys.
function pick<T extends object, K extends keyof T>(obj: T, keys: readonly K[]): Pick<T, K>;Example:
pick({ a: 1, b: 2 }, ['a']) // => { a: 1 }pickBy
Keeps entries whose value and key pass predicate.
function pickBy<T extends object>(obj: T, predicate: (value: T[keyof T], key: keyof T & string) => boolean): Partial<T>;Example:
pickBy({ a: 1, b: null }, (v) => v !== null) // => { a: 1 }renameKeys
Renames keys by a mapping; unmapped keys are kept. Later entries win on collisions.
function renameKeys(obj: object, mapping: Readonly<Record<string, string>>): Record<string, unknown>;Example:
renameKeys({ first: 'A' }, { first: 'givenName' }) // => { givenName: 'A' }size
Element count: length of arrays/strings, size of maps/sets, own enumerable keys of objects, 0 for nullish.
function size(value: unknown): number;Example:
size({ a: 1, b: 2 }) // => 2sortKeys
Returns a copy with keys inserted in sorted order (for canonical output). Note that JavaScript always enumerates integer-like keys first, in ascending numeric order.
function sortKeys<T extends object>(obj: T, options?: SortKeysOptions): T;Example:
Object.keys(sortKeys({ b: 1, a: 2 })) // => ['a', 'b']typedEntries
Object.entries typed.
function typedEntries<T extends object>(obj: T): [keyof T & string, T[keyof T]][];Example:
typedEntries({ a: 1 }) // => [['a', 1]]typedFromEntries
Object.fromEntries typed (defines own data properties; never calls the __proto__ setter).
function typedFromEntries<K extends PropertyKey, V>(entries: Iterable<readonly [K, V]>): Record<K, V>;Example:
typedFromEntries([['a', 1]] as const) // => { a: 1 }typedKeys
Object.keys typed as the object’s string keys.
function typedKeys<T extends object>(obj: T): (keyof T & string)[];Example:
typedKeys({ a: 1 }) // => ['a']typedValues
Object.values typed.
function typedValues<T extends object>(obj: T): T[keyof T][];Example:
typedValues({ a: 1 }) // => [1]Interfaces
Section titled “Interfaces”CompactObjectOptions
interface CompactObjectOptions { readonly remove?: 'nullish' | 'undefined' | 'empty'; readonly deep?: boolean;}SortKeysOptions
interface SortKeysOptions { readonly recursive?: boolean; readonly compare?: (a: string, b: string) => number;}Functions
Section titled “Functions”deepClone
Deep copy supporting Date, RegExp, Map, Set, ArrayBuffer, typed arrays, DataView and cycles. Functions are copied by reference.
function deepClone<T>(value: T, options?: DeepCloneOptions): T;Example:
const b = deepClone({ d: new Date(0), s: new Set([1]) });deepEqual
Structural equality: key-order independent; handles NaN, Date, RegExp, Map, Set, ArrayBuffer, typed arrays, DataView, boxed primitives, errors and cycles. Objects must share a prototype.
function deepEqual(a: unknown, b: unknown, options?: DeepEqualOptions): boolean;Example:
deepEqual({ a: [1, { b: NaN }] }, { a: [1, { b: NaN }] }) // => truedeepFreeze
Freezes value and everything reachable through own properties, map/set entries (cycle-safe).
Typed arrays cannot be frozen and are left as is. Mutates (freezes) the input and returns it.
function deepFreeze<T>(value: T): DeepReadonly<T>;Example:
const cfg = deepFreeze({ a: { b: 1 } });Object.isFrozen(cfg.a); // => true // assigning cfg.a.b throws in strict modedeepGet
Reads a nested own property. Returns fallback when the path is missing or resolves to undefined.
Never traverses into prototypes (deepGet({}, 'constructor') is fallback).
function deepGet(obj: unknown, path: Path, fallback?: unknown): unknown;Example:
deepGet({ a: [{ b: 1 }] }, 'a[0].b') // => 1deepHas
True when every segment of the path is an own property.
function deepHas(obj: unknown, path: Path): boolean;Example:
deepHas({ a: { b: undefined } }, 'a.b') // => truedeepMerge
Deep-merges plain objects into a new object. Only plain objects merge; other values (class instances, dates, maps) replace. Prototypes are never merged or modified.
function deepMerge<A extends object, B extends object>(target: A, source: B, options?: DeepMergeOptions): A & B;Example:
deepMerge({ a: { x: 1 } }, { a: { y: 2 } }) // => { a: { x: 1, y: 2 } }deepMergeAll
Deep-merges several objects left to right into a new object.
function deepMergeAll(objects: readonly object[], options?: DeepMergeOptions): Record<string, unknown>;Example:
deepMergeAll([{ a: 1 }, { b: 2 }, { a: 3 }]) // => { a: 3, b: 2 }deepSet
Returns a copy with the value set at path, copying only the containers along the path.
Missing or primitive intermediates become [] (numeric next segment) or {}.
Throws HLP_UNSAFE_KEY for __proto__/constructor/prototype segments and
HLP_OBJECT_PATH_BLOCKED when the path crosses a non-plain object (Date, Map, class instance).
function deepSet<T>(obj: T, path: Path, value: unknown): T;Example:
deepSet({}, 'a.list[1]', 'x') // => { a: { list: [undefined, 'x'] } } // index 0 is a holedeepSetInPlace
Sets the value at path, mutating obj and creating missing containers. Returns obj.
function deepSetInPlace<T extends object>(obj: T, path: Path, value: unknown): T;Example:
const o = {};deepSetInPlace(o, 'a.b', 1);o; // => { a: { b: 1 } }deepUnset
Returns a copy without the property at path (array elements are spliced out).
Returns obj itself when the path does not exist.
function deepUnset<T>(obj: T, path: Path): T;Example:
deepUnset({ a: { b: 1, c: 2 } }, 'a.b') // => { a: { c: 2 } }Interfaces
Section titled “Interfaces”DeepCloneOptions
interface DeepCloneOptions { readonly strategy?: 'auto' | 'manual';}DeepEqualOptions
interface DeepEqualOptions { readonly distinguishZero?: boolean;}DeepMergeOptions
interface DeepMergeOptions { readonly arrays?: 'replace' | 'concat' | 'merge-by-index' | 'union'; readonly undefinedValues?: 'skip' | 'overwrite'; readonly unsafeKeys?: 'error' | 'skip';}DeepReadonly
Recursively readonly view of T.
type DeepReadonly<T> = T extends (...args: never[]) => unknown ? T : T extends ReadonlyMap<infer K, infer V> ? ReadonlyMap<DeepReadonly<K>, DeepReadonly<V>> : T extends ReadonlySet<infer U> ? ReadonlySet<DeepReadonly<U>> : T extends object ? { readonly [P in keyof T]: DeepReadonly<T[P]>;} : T;Functions
Section titled “Functions”applyDiff
Applies diff changes and returns a new value; the input is never mutated and only
containers on changed paths are copied. add inserts into arrays (index <= length);
remove splices arrays. Throws HLP_OBJECT_PATH_NOT_FOUND for missing paths and
HLP_UNSAFE_KEY for unsafe segments.
function applyDiff<T>(obj: T, changes: readonly DiffChange[]): T;Example:
applyDiff({ a: 1 }, diff({ a: 1 }, { a: 2 })) // => { a: 2 }diff
Lists the changes that turn a into b. Plain objects and arrays are compared recursively
(arrays by index: appended items are adds in ascending order, dropped tail items are removes
in descending order); other values are compared with deepEqual and reported as replace.
function diff(a: unknown, b: unknown): DiffChange[];Example:
diff({ a: 1 }, { a: 2, b: 3 }) // => [{ op: 'replace', path: ['a'], from: 1, value: 2 }, { op: 'add', path: ['b'], value: 3 }]flattenObject
Flattens nested plain objects (and arrays) to one level with joined keys.
Empty objects and arrays are kept as values so unflattenObject restores them.
function flattenObject(obj: object, options?: FlattenOptions): Record<string, unknown>;Example:
flattenObject({ a: { b: 1, c: [2] } }) // => { 'a.b': 1, 'a.c.0': 2 }unflattenObject
Rebuilds nested objects from joined keys. Every segment is checked with assertSafeKey,
so { '__proto__.x': 1 } throws HLP_UNSAFE_KEY. Later keys win on conflicts.
function unflattenObject(flat: Readonly<Record<string, unknown>>, options?: UnflattenOptions): Record<string, unknown>;Example:
unflattenObject({ 'a.b': 1, 'a.c.0': 2 }) // => { a: { b: 1, c: [2] } }Interfaces
Section titled “Interfaces”FlattenOptions
interface FlattenOptions { readonly separator?: string; readonly arrays?: boolean;}UnflattenOptions
interface UnflattenOptions { readonly separator?: string; readonly arrays?: boolean; readonly maxArrayIndex?: number;}DiffChange
One change produced by diff. Paths are segment arrays; [] is the root.
type DiffChange = { readonly op: 'add'; readonly path: readonly PathSegment[]; readonly value: unknown;} | { readonly op: 'remove'; readonly path: readonly PathSegment[]; readonly from: unknown;} | { readonly op: 'replace'; readonly path: readonly PathSegment[]; readonly from: unknown; readonly value: unknown;};Functions
Section titled “Functions”formatPath
Formats segments as a path string that parsePath reads back exactly.
function formatPath(segments: readonly PathSegment[]): string;Example:
formatPath(['a', 0, 'x.y']) // => 'a[0]["x.y"]'parsePath
Parses a path into segments. Canonical digit segments (0, 12) become numbers;
quoted brackets (["a.b"]) keep any characters, with \ escaping.
function parsePath(path: Path): PathSegment[];Example:
parsePath('a.b[0]["x.y"]') // => ['a', 'b', 0, 'x.y']Constants
Section titled “Constants”MAX_PATH_LENGTH
Maximum path length in characters (string paths) or segments (array paths).
const MAX_PATH_LENGTH = 4096;Path
A dotted/bracketed path string or an array of segments.
type Path = string | readonly PathSegment[];PathSegment
One path step: a property name or an array index.
type PathSegment = string | number;