node-comfortv2.0.0

nc.obj

Object helpers: deep clone, merge, equality, diff, typed dot paths, pick and omit. Inputs are never mutated, and __proto__, constructor and prototype keys are always ignored, so user input can't pollute prototypes.

const { obj } = require("@ix-xs/node-comfort");
import { clone } from "@ix-xs/node-comfort/obj";
obj.get(config, "db.pool.max", 10);         // typed, paths autocompleted
obj.set(state, "user.profile.name", "Ada"); // returns an updated copy
obj.merge(defaults, userConfig);
GuideExplanations and examples for nc.obj.
Read the guide →

Functions

nc.obj.clone()

clone<T>(value: T): T

Deep copy. Uses structuredClone when it can, and falls back to a copy that keeps functions and class prototypes.

Parameters

NameType
valueT

Returns T

Example

const copy = obj.clone({ a: { b: [1, 2] }, when: new Date() });
copy.a.b.push(3); // the original is untouched

nc.obj.merge()4 overloads

merge<A extends object>(a: A): A
merge<A extends object, B extends object>(a: A, b: B): A & B
merge<A extends object, B extends object, C extends object>(a: A, b: B, c: C): A & B & C
merge(...sources: object[]): Record<string, any>

Deep-merges objects into a new one. Later objects win, nested objects are merged, arrays are replaced. See mergeWith() to join arrays instead.

Parameters

NameType
aA
bB
cC

Returns A & B & C

Example

obj.merge({ db: { host: "localhost", port: 5432 } }, { db: { port: 6543 } });
// { db: { host: "localhost", port: 6543 } }

nc.obj.mergeWith()

mergeWith(options: MergeOptions, ...sources: object[]): Record<string, any>

Like merge(), with a choice of what happens to arrays and undefined.

Parameters

NameType
optionsMergeOptions
...sources...object

Returns Record<string, any>

Example

obj.mergeWith({ arrays: "unique" }, { tags: ["a", "b"] }, { tags: ["b", "c"] });
// { tags: ["a", "b", "c"] }
obj.mergeWith({ skipUndefined: true }, { port: 80 }, { port: undefined });
// { port: 80 }

nc.obj.defaults()

defaults<T extends object, D extends object>(target: T, ...sources: D[]): T & D

Fills in missing properties from defaults, deeply. Values that are set, even null or 0, are kept.

Parameters

NameTypeDescription
targetT
...sources...DThe first one has the highest priority.

Returns T & D

Example

obj.defaults({ port: 8080, db: { user: "me" } }, { port: 80, db: { user: "root", pass: "" } });
// { port: 8080, db: { user: "me", pass: "" } }

nc.obj.equal()

equal(a: unknown, b: unknown): boolean

Deep equality. Knows about arrays, dates, regexes, maps, sets, typed arrays, NaN and circular references.

Parameters

NameType
aunknown
bunknown

Returns boolean

Example

obj.equal({ a: [1, { b: 2 }] }, { a: [1, { b: 2 }] }); // true
obj.equal(new Set([1, 2]), new Set([2, 1]));          // true
obj.equal({ a: 1 }, { a: 1, b: undefined });          // false

nc.obj.diff()

diff(before: unknown, after: unknown): Change[]

Lists what changed between two values, deeply. Handy for audit logs and partial updates.

Parameters

NameType
beforeunknown
afterunknown

Returns Change[]: Empty when nothing changed.

Example

obj.diff({ name: "Ada", tags: ["a"] }, { name: "Ada L.", tags: ["a", "b"] });
// [
//   { path: ["name"], type: "changed", from: "Ada", to: "Ada L." },
//   { path: ["tags", 1], type: "added", to: "b" },
// ]

nc.obj.get()2 overloads

get<T, P extends string>(object: T, path: P | (T extends PathLeaf ? never : T extends object ? { [K in keyof T & (string | number)]: NonNullable<T[K]> extends PathLeaf ? `${K}` : NonNullable<T[K]> extends object ? `${K}` | `${K}.${NonNullable<T[K]> extends PathLeaf ? never : NonNullable<T[K]> extends object ? { [K in keyof NonNullable<T[K]> & (string | number)]: NonNullable<NonNullable<T[K]>[K]> extends PathLeaf ? `${K}` : NonNullable<NonNullable<T[K]>[K]> extends object ? `${K}` | `${K}.${NonNullable<NonNullable<T[K]>[K]> extends PathLeaf ? never : NonNullable<NonNullable<T[K]>[K]> extends object ? { [K in keyof NonNullable<NonNullable<T[K]>[K]> & (string | number)]: NonNullable<NonNullable<NonNullable<T[K]>[K]>[K]> extends PathLeaf ? `${K}` : NonNullable<NonNullable<NonNullable<T[K]>[K]>[K]> extends object ? `${K}` | `${K}.${NonNullable<NonNullable<NonNullable<T[K]>[K]>[K]> extends PathLeaf ? never : NonNullable<NonNullable<NonNullable<T[K]>[K]>[K]> extends object ? { [K in keyof NonNullable<NonNullable<NonNullable<T[K]>[K]>[K]> & (string | number)]: NonNullable<NonNullable<NonNullable<NonNullable<T[K]>[K]>[K]>[K]> extends PathLeaf ? `${K}` : NonNullable<NonNullable<NonNullable<NonNullable<T[K]>[K]>[K]>[K]> extends object ? `${K}` | `${K}.${NonNullable<NonNullable<NonNullable<NonNullable<T[K]>[K]>[K]>[K]> extends PathLeaf ? never : NonNullable<NonNullable<NonNullable<NonNullable<T[K]>[K]>[K]>[K]> extends object ? { [K in keyof NonNullable<NonNullable<NonNullable<NonNullable<T[K]>[K]>[K]>[K]> & (string | number)]: NonNullable<NonNullable<NonNullable<NonNullable<NonNullable<T[K]>[K]>[K]>[K]>[K]> extends PathLeaf ? `${K}` : NonNullable<NonNullable<NonNullable<NonNullable<NonNullable<T[K]>[K]>[K]>[K]>[K]> extends object ? `${K}` | `${K}.${NonNullable<NonNullable<NonNullable<NonNullable<NonNullable<T[K]>[K]>[K]>[K]>[K]> extends PathLeaf ? never : NonNullable<NonNullable<NonNullable<NonNullable<NonNullable<T[K]>[K]>[K]>[K]>[K]> extends object ? { [K in keyof NonNullable<NonNullable<NonNullable<NonNullable<NonNullable<T[K]>[K]>[K]>[K]>[K]> & (string | number)]: NonNullable<NonNullable<NonNullable<NonNullable<NonNullable<NonNullable<T[K]>[K]>[K]>[K]>[K]>[K]> extends PathLeaf ? `${K}` : NonNullable<NonNullable<NonNullable<NonNullable<NonNullable<NonNullable<T[K]>[K]>[K]>[K]>[K]>[K]> extends object ? `${K}` : `${K}`; }[keyof NonNullable<NonNullable<NonNullable<NonNullable<NonNullable<T[K]>[K]>[K]>[K]>[K]> & (string | number)] : never}` : `${K}`; }[keyof NonNullable<NonNullable<NonNullable<NonNullable<T[K]>[K]>[K]>[K]> & (string | number)] : never}` : `${K}`; }[keyof NonNullable<NonNullable<NonNullable<T[K]>[K]>[K]> & (string | number)] : never}` : `${K}`; }[keyof NonNullable<NonNullable<T[K]>[K]> & (string | number)] : never}` : `${K}`; }[keyof NonNullable<T[K]> & (string | number)] : never}` : `${K}`; }[keyof T & (string | number)] : never) | readonly (string | number)[]): PathValue<T, P>
get<T, P extends string, F>(object: T, path: P | readonly (string | number)[] | (T extends PathLeaf ? never : T extends object ? { [K in keyof T & (string | number)]: NonNullable<T[K]> extends PathLeaf ? `${K}` : NonNullable<T[K]> extends object ? `${K}` | `${K}.${NonNullable<T[K]> extends PathLeaf ? never : NonNullable<T[K]> extends object ? { [K in keyof NonNullable<T[K]> & (string | number)]: NonNullable<NonNullable<T[K]>[K]> extends PathLeaf ? `${K}` : NonNullable<NonNullable<T[K]>[K]> extends object ? `${K}` | `${K}.${NonNullable<NonNullable<T[K]>[K]> extends PathLeaf ? never : NonNullable<NonNullable<T[K]>[K]> extends object ? { [K in keyof NonNullable<NonNullable<T[K]>[K]> & (string | number)]: NonNullable<NonNullable<NonNullable<T[K]>[K]>[K]> extends PathLeaf ? `${K}` : NonNullable<NonNullable<NonNullable<T[K]>[K]>[K]> extends object ? `${K}` | `${K}.${NonNullable<NonNullable<NonNullable<T[K]>[K]>[K]> extends PathLeaf ? never : NonNullable<NonNullable<NonNullable<T[K]>[K]>[K]> extends object ? { [K in keyof NonNullable<NonNullable<NonNullable<T[K]>[K]>[K]> & (string | number)]: NonNullable<NonNullable<NonNullable<NonNullable<T[K]>[K]>[K]>[K]> extends PathLeaf ? `${K}` : NonNullable<NonNullable<NonNullable<NonNullable<T[K]>[K]>[K]>[K]> extends object ? `${K}` | `${K}.${NonNullable<NonNullable<NonNullable<NonNullable<T[K]>[K]>[K]>[K]> extends PathLeaf ? never : NonNullable<NonNullable<NonNullable<NonNullable<T[K]>[K]>[K]>[K]> extends object ? { [K in keyof NonNullable<NonNullable<NonNullable<NonNullable<T[K]>[K]>[K]>[K]> & (string | number)]: NonNullable<NonNullable<NonNullable<NonNullable<NonNullable<T[K]>[K]>[K]>[K]>[K]> extends PathLeaf ? `${K}` : NonNullable<NonNullable<NonNullable<NonNullable<NonNullable<T[K]>[K]>[K]>[K]>[K]> extends object ? `${K}` | `${K}.${NonNullable<NonNullable<NonNullable<NonNullable<NonNullable<T[K]>[K]>[K]>[K]>[K]> extends PathLeaf ? never : NonNullable<NonNullable<NonNullable<NonNullable<NonNullable<T[K]>[K]>[K]>[K]>[K]> extends object ? { [K in keyof NonNullable<NonNullable<NonNullable<NonNullable<NonNullable<T[K]>[K]>[K]>[K]>[K]> & (string | number)]: NonNullable<NonNullable<NonNullable<NonNullable<NonNullable<NonNullable<T[K]>[K]>[K]>[K]>[K]>[K]> extends PathLeaf ? `${K}` : NonNullable<NonNullable<NonNullable<NonNullable<NonNullable<NonNullable<T[K]>[K]>[K]>[K]>[K]>[K]> extends object ? `${K}` : `${K}`; }[keyof NonNullable<NonNullable<NonNullable<NonNullable<NonNullable<T[K]>[K]>[K]>[K]>[K]> & (string | number)] : never}` : `${K}`; }[keyof NonNullable<NonNullable<NonNullable<NonNullable<T[K]>[K]>[K]>[K]> & (string | number)] : never}` : `${K}`; }[keyof NonNullable<NonNullable<NonNullable<T[K]>[K]>[K]> & (string | number)] : never}` : `${K}`; }[keyof NonNullable<NonNullable<T[K]>[K]> & (string | number)] : never}` : `${K}`; }[keyof NonNullable<T[K]> & (string | number)] : never}` : `${K}`; }[keyof T & (string | number)] : never), fallback: F): F | Exclude<PathValue<T, P>, undefined>

Reads a nested value from a dotted path. Your editor completes the paths and knows the type of the result.

Parameters

NameTypeDescription
objectT
pathP | Paths<T> | ReadonlyArray<string | number>Like "a.b.c", "a[0].b" or ["a", 0, "b"].
fallbackF

Returns Exclude<PathValue<T, P>, undefined> | F

Example

obj.get(config, "db.pool.max");      // number | undefined
obj.get(config, "db.pool.max", 10);  // number
obj.get(data, "users[0].name");
obj.get(data, ["users", 0, "name"]);

nc.obj.set()

set<T>(object: T, path: PathInput<T>, value: unknown): T

Returns a copy with a nested value set. Missing levels are created (arrays when the next key is a number).

Parameters

NameType
objectT
pathPathInput<T>
valueunknown

Returns T

Example

obj.set({}, "a.b.c", 1);            // { a: { b: { c: 1 } } }
obj.set(state, "user.name", "Ada"); // state is untouched

nc.obj.unset()

unset<T>(object: T, path: PathInput<T>): T

Returns a copy without the property at path.

Parameters

NameType
objectT
pathPathInput<T>

Returns T

Example

obj.unset({ a: { b: 1, c: 2 } }, "a.b"); // { a: { c: 2 } }

nc.obj.has()

has<T>(object: T, path: PathInput<T>): boolean

Does the path exist? A property set to undefined still counts.

Parameters

NameType
objectT
pathPathInput<T>

Returns boolean

Example

obj.has({ a: { b: undefined } }, "a.b"); // true
obj.has({ a: {} }, "a.b");               // false

nc.obj.pick()

pick<T extends object, K extends keyof T>(object: T, keys: readonly K[]): Pick<T, K>

A copy with only the listed keys.

Parameters

NameType
objectT
keysreadonly K[]

Returns Pick<T, K>

Example

obj.pick(user, ["id", "email"]);

nc.obj.omit()

omit<T extends object, K extends keyof T>(object: T, keys: readonly K[]): Omit<T, K>

A copy without the listed keys.

Parameters

NameType
objectT
keysreadonly K[]

Returns Omit<T, K>

Example

obj.omit(user, ["password", "token"]);

nc.obj.filter()

filter<T extends object>(object: T, predicate: (value: T[keyof T], key: keyof T & string) => unknown): Partial<T>

Keeps the entries that pass a test.

Parameters

NameType
objectT
predicate(value: T[keyof T], key: keyof T & string) => unknown

Returns Partial<T>

Example

obj.filter({ a: 1, b: 2, c: 3 }, (value) => value > 1); // { b: 2, c: 3 }

nc.obj.mapValues()

mapValues<T extends object, R>(object: T, fn: (value: T[keyof T], key: keyof T & string) => R): { [K in keyof T]: R; }

Transforms every value, keeping the keys.

Parameters

NameType
objectT
fn(value: T[keyof T], key: keyof T & string) => R

Returns { [K in keyof T]: R }

Example

obj.mapValues({ a: 1, b: 2 }, (v) => v * 10); // { a: 10, b: 20 }

nc.obj.mapKeys()

mapKeys<T extends object>(object: T, fn: (key: keyof T & string, value: T[keyof T]) => PropertyKey): Record<string, T[keyof T]>

Transforms every key, keeping the values.

Parameters

NameTypeDescription
objectT
fn(key: keyof T & string, value: T[keyof T]) => PropertyKeyReturns the new key.

Returns Record<string, T[keyof T]>

Example

obj.mapKeys({ first_name: "Ada" }, (key) => nc.str.camelCase(key)); // { firstName: "Ada" }

nc.obj.renameKeys()

renameKeys<T extends object>(object: T, mapping: Partial<Record<keyof T, string>>): Record<string, T[keyof T]>

Renames some keys and leaves the others alone.

Parameters

NameTypeDescription
objectT
mappingPartial<Record<keyof T, string>>Old name to new name.

Returns Record<string, T[keyof T]>

Example

obj.renameKeys({ _id: 1, name: "Ada" }, { _id: "id" }); // { id: 1, name: "Ada" }

nc.obj.invert()

invert(object: Record<string, PropertyKey>): Record<string, string>

Swaps keys and values.

Parameters

NameType
objectRecord<string, PropertyKey>

Returns Record<string, string>

Example

obj.invert({ a: "x", b: "y" }); // { x: "a", y: "b" }

nc.obj.compact()

compact<T extends object>(object: T, options?: CompactOptions): Partial<T>

Removes null and undefined values, and optionally empty ones.

Parameters

NameType
objectT
optionsoptionalCompactOptions

Returns Partial<T>

Example

obj.compact({ a: 1, b: null, c: undefined });                          // { a: 1 }
obj.compact({ a: { b: null, c: "" } }, { deep: true, removeEmpty: true }); // {}

nc.obj.flatten()

flatten(object: object, prefix?: string): Record<string, unknown>

Flattens nested objects into dotted keys. Arrays stay as values.

Parameters

NameTypeDescription
objectobject
prefixoptionalstringAdded in front of every key.Default: ""

Returns Record<string, unknown>

Example

obj.flatten({ db: { host: "x", port: 5432 } }); // { "db.host": "x", "db.port": 5432 }

nc.obj.unflatten()

unflatten(object: Record<string, unknown>): Record<string, any>

Rebuilds nested objects from dotted keys. The opposite of flatten().

Parameters

NameType
objectRecord<string, unknown>

Returns Record<string, any>

Example

obj.unflatten({ "db.host": "x", "db.port": 5432 }); // { db: { host: "x", port: 5432 } }

nc.obj.keys()

keys<T extends object>(object: T): (keyof T & string)[]

Object.keys, but typed with the object's actual keys.

Parameters

NameType
objectT

Returns Array<keyof T & string>

Example

for (const key of obj.keys(config)) config[key]; // no type error

nc.obj.entries()

entries<T extends object>(object: T): [keyof T & string, T[keyof T]][]

Object.entries, with typed keys and values.

Parameters

NameType
objectT

Returns Array<[keyof T & string, T[keyof T]]>

nc.obj.fromEntries()

fromEntries<K extends PropertyKey, V>(pairs: Iterable<readonly [K, V]>): Record<K, V>

Object.fromEntries that accepts any iterable (a Map too) and skips unsafe keys.

Parameters

NameType
pairsIterable<readonly [K, V]>

Returns Record<K, V>

Example

obj.fromEntries(new Map([["x", true]])); // { x: true }

nc.obj.size()

size(value: unknown): number

How many entries: own keys of an object, size of a map or set, length of an array or string.

Parameters

NameType
valueunknown

Returns number

Example

obj.size({ a: 1, b: 2 }); // 2

nc.obj.isEmpty()

isEmpty(object: unknown): boolean

Does the object have no own keys? null and undefined count as empty.

Parameters

NameType
objectunknown

Returns boolean

nc.obj.deepFreeze()

deepFreeze<T>(object: T): DeepReadonly<T>

Freezes an object and everything inside it. Good for configuration and constants.

Parameters

NameType
objectT

Returns DeepReadonly<T>: The same object.

Example

const CONFIG = obj.deepFreeze({ db: { host: "localhost" } });
CONFIG.db.host = "x"; // TypeScript error, and ignored at runtime

Types

Import any of them in TypeScript with import type { Change } from "@ix-xs/node-comfort", or in JavaScript with import("@ix-xs/node-comfort").Change.

Change

One difference reported by diff().

PropertyTypeDescription
path(string | number)[]Where it changed, like ["address", "city"].
type"added" | "removed" | "changed"
fromoptionalunknownThe old value.
tooptionalunknownThe new value.

CompactOptions

Options for compact().

PropertyTypeDescription
deepoptionalbooleanClean nested objects and arrays too.
removeEmptyoptionalbooleanAlso remove "", [] and {}.

DeepReadonly

T, read-only all the way down.

type DeepReadonly = T extends (...args: any[]) => any ? T : T extends object ? { readonly [K in keyof T]: DeepReadonly<T[K]>; } : T

MergeOptions

Options for mergeWith().

PropertyTypeDescription
arraysoptional"concat" | "replace" | "unique"When both sides have an array: keep the last, join them, or join without duplicates. Defaults to "replace".
skipUndefinedoptionalbooleanDon't let undefined overwrite an existing value.

NullishToUndefined

type NullishToUndefined = [Extract<T, null | undefined>] extends [never] ? never : undefined

PathInput

A path: a known dotted path (completed by your editor), any other string like "items[0].name", or an array of keys.

type PathInput = Paths<T> | (string & {}) | ReadonlyArray<string | number>

PathLeaf

Values that end a path: dates, regexes, maps, sets, functions and arrays.

type PathLeaf = Date | RegExp | Map<any, any> | Set<any> | ((...args: any[]) => any) | readonly any[]

Paths

Every dotted path of an object type, up to 6 levels. This is what powers path completion in obj.get() and friends.

type Paths = Depth["length"] extends 6 ? never : T extends PathLeaf ? never : T extends object ? { [K in keyof T & (string | number)]: NonNullable<T[K]> extends PathLeaf ? `${K}` : NonNullable<T[K]> extends object ? `${K}` | `${K}.${Paths<NonNullable<T[K]>, [...Depth, 0]>}` : `${K}`; }[keyof T & (string | number)] : never

PathStep

type PathStep = K extends keyof NonNullable<T> ? NonNullable<T>[K] : NonNullable<T> extends readonly (infer E)[] ? (K extends `${number}` ? E | undefined : typeof _NOT_FOUND) : typeof _NOT_FOUND

PathValue

The type found at a dotted path of T, or any for an unknown path.

type PathValue = P extends `${infer Head}.${infer Rest}` ? PathStep<T, Head> extends infer Next ? ([Next] extends [typeof _NOT_FOUND] ? any : PathValue<Next, Rest> | NullishToUndefined<T>) : any : PathStep<T, P> extends infer Next ? ([Next] extends [typeof _NOT_FOUND] ? any : Next | NullishToUndefined<T>) : any
node-comfort v2.0.0View the source