Skip to content

@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';

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' }

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_KEY
deepSet({}, 'constructor.prototype.isAdmin', true); // => throws HLP_UNSAFE_KEY
unflattenObject({ '__proto__.isAdmin': true }); // => throws HLP_UNSAFE_KEY

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') // => undefined

invert

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 }) // => true

mapEntries

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') // => 1

omit

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 }) // => 2

sortKeys

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]

CompactObjectOptions

interface CompactObjectOptions {
readonly remove?: 'nullish' | 'undefined' | 'empty';
readonly deep?: boolean;
}

SortKeysOptions

interface SortKeysOptions {
readonly recursive?: boolean;
readonly compare?: (a: string, b: string) => number;
}

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 }] }) // => true

deepFreeze

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 mode

deepGet

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') // => 1

deepHas

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') // => true

deepMerge

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 hole

deepSetInPlace

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 } }

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;

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] } }

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;
};

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']

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;