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);nc.obj.Functions
nc.obj.clone()
clone<T>(value: T): TDeep copy. Uses structuredClone when it can, and falls back to a copy that keeps functions and class prototypes.
Parameters
| Name | Type |
|---|---|
value | T |
Returns T
Example
const copy = obj.clone({ a: { b: [1, 2] }, when: new Date() });
copy.a.b.push(3); // the original is untouchednc.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
| Name | Type |
|---|---|
a | A |
b | B |
c | C |
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
| Name | Type |
|---|---|
options | MergeOptions |
...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 & DFills in missing properties from defaults, deeply. Values that are set, even null or 0, are kept.
Parameters
| Name | Type | Description |
|---|---|---|
target | T | |
...sources | ...D | The 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): booleanDeep equality. Knows about arrays, dates, regexes, maps, sets, typed arrays, NaN and circular references.
Parameters
| Name | Type |
|---|---|
a | unknown |
b | unknown |
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 }); // falsenc.obj.diff()
diff(before: unknown, after: unknown): Change[]Lists what changed between two values, deeply. Handy for audit logs and partial updates.
Parameters
| Name | Type |
|---|---|
before | unknown |
after | unknown |
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
| Name | Type | Description |
|---|---|---|
object | T | |
path | P | Paths<T> | ReadonlyArray<string | number> | Like "a.b.c", "a[0].b" or ["a", 0, "b"]. |
fallback | F |
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): TReturns a copy with a nested value set. Missing levels are created (arrays when the next key is a number).
Parameters
| Name | Type |
|---|---|
object | T |
path | PathInput<T> |
value | unknown |
Returns T
Example
obj.set({}, "a.b.c", 1); // { a: { b: { c: 1 } } }
obj.set(state, "user.name", "Ada"); // state is untouchednc.obj.unset()
unset<T>(object: T, path: PathInput<T>): TReturns a copy without the property at path.
Parameters
| Name | Type |
|---|---|
object | T |
path | PathInput<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>): booleanDoes the path exist? A property set to undefined still counts.
Parameters
| Name | Type |
|---|---|
object | T |
path | PathInput<T> |
Returns boolean
Example
obj.has({ a: { b: undefined } }, "a.b"); // true
obj.has({ a: {} }, "a.b"); // falsenc.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
| Name | Type |
|---|---|
object | T |
keys | readonly 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
| Name | Type |
|---|---|
object | T |
keys | readonly 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
| Name | Type |
|---|---|
object | T |
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
| Name | Type |
|---|---|
object | T |
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
| Name | Type | Description |
|---|---|---|
object | T | |
fn | (key: keyof T & string, value: T[keyof T]) => PropertyKey | Returns 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
| Name | Type | Description |
|---|---|---|
object | T | |
mapping | Partial<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
| Name | Type |
|---|---|
object | Record<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
| Name | Type |
|---|---|
object | T |
optionsoptional | CompactOptions |
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
| Name | Type | Description |
|---|---|---|
object | object | |
prefixoptional | string | Added 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
| Name | Type |
|---|---|
object | Record<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
| Name | Type |
|---|---|
object | T |
Returns Array<keyof T & string>
Example
for (const key of obj.keys(config)) config[key]; // no type errornc.obj.entries()
entries<T extends object>(object: T): [keyof T & string, T[keyof T]][]Object.entries, with typed keys and values.
Parameters
| Name | Type |
|---|---|
object | T |
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
| Name | Type |
|---|---|
pairs | Iterable<readonly [K, V]> |
Returns Record<K, V>
Example
obj.fromEntries(new Map([["x", true]])); // { x: true }nc.obj.size()
size(value: unknown): numberHow many entries: own keys of an object, size of a map or set, length of an array or string.
Parameters
| Name | Type |
|---|---|
value | unknown |
Returns number
Example
obj.size({ a: 1, b: 2 }); // 2nc.obj.isEmpty()
isEmpty(object: unknown): booleanDoes the object have no own keys? null and undefined count as empty.
Parameters
| Name | Type |
|---|---|
object | unknown |
Returns boolean
nc.obj.deepFreeze()
deepFreeze<T>(object: T): DeepReadonly<T>Freezes an object and everything inside it. Good for configuration and constants.
Parameters
| Name | Type |
|---|---|
object | T |
Returns DeepReadonly<T>: The same object.
Example
const CONFIG = obj.deepFreeze({ db: { host: "localhost" } });
CONFIG.db.host = "x"; // TypeScript error, and ignored at runtimeTypes
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().
| Property | Type | Description |
|---|---|---|
path | (string | number)[] | Where it changed, like ["address", "city"]. |
type | "added" | "removed" | "changed" | |
fromoptional | unknown | The old value. |
tooptional | unknown | The new value. |
CompactOptions
Options for compact().
| Property | Type | Description |
|---|---|---|
deepoptional | boolean | Clean nested objects and arrays too. |
removeEmptyoptional | boolean | Also 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]>; } : TMergeOptions
Options for mergeWith().
| Property | Type | Description |
|---|---|---|
arraysoptional | "concat" | "replace" | "unique" | When both sides have an array: keep the last, join them, or join without duplicates. Defaults to "replace". |
skipUndefinedoptional | boolean | Don't let undefined overwrite an existing value. |
NullishToUndefined
type NullishToUndefined = [Extract<T, null | undefined>] extends [never] ? never : undefinedPathInput
A path: a known dotted path (completed by your editor), any other string like "items[0].name", or an array of keys.
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)] : neverPathStep
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_FOUNDPathValue
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