A fast, safe, and lightweight utility library for deep object access, deep object mutation, and deep merging in JavaScript and TypeScript — designed as a modern drop-in replacement for
lodash.get,lodash.set, andlodash.merge.
Working with deeply nested objects in JavaScript usually means one of two things:
-
Messy manual chaining that throws on
null:const name = user && user.profile && user.profile.name; // 😩
-
Pulling in all of lodash for three utilities:
const _ = require("lodash"); _.get(obj, "user.profile.name"); // ~70 KB for 3 functions
objx replaces the most-used 20% of lodash with a focused, typed, 10× better experience — in under 2 KB gzipped.
| Feature | lodash | objx |
|---|---|---|
| Deep get | ✅ | ✅ |
| Deep set | ✅ | ✅ |
| Deep merge | ✅ | ✅ |
| Immutable updates | ❌ | ✅ |
| Prototype-pollution safe | ✅ | |
| Typed paths (TypeScript) | ❌ | ✅ |
| Path compiler (hot-path perf) | ❌ | ✅ |
| Tree-shakable | ❌ | ✅ |
| ESM + CJS | partial | ✅ |
| Bundle size | ~70 KB | < 2 KB |
npm install @munesoft/objx
# or
yarn add @munesoft/objx
# or
pnpm add @munesoft/objximport * as objx from "@munesoft/objx";
const user = {
profile: { name: "Alice", age: 30 },
tags: ["admin", "editor"],
};
// Deep get — never throws, returns fallback on missing path
objx.get(user, "profile.name"); // "Alice"
objx.get(user, "profile.address.city", "?"); // "?"
objx.get(user, "tags[0]"); // "admin"
// Deep set — creates missing intermediaries
objx.set(user, "profile.location.city", "Nairobi");
// Immutable deep set — returns new object, original untouched
const next = objx.set(user, "profile.name", "Bob", { immutable: true });
user.profile.name; // still "Alice"
// Deep merge — prototype-pollution safe
const merged = objx.merge({ db: { host: "localhost" } }, { db: { port: 5432 } });
// { db: { host: "localhost", port: 5432 } }
// Check if path exists
objx.has(user, "profile.name"); // true
objx.has(user, "profile.email"); // false
// Delete a nested key
objx.del(user, "profile.age");
// Apply defaults (fills only undefined keys, recursively)
objx.defaults(config, defaultConfig);Safely retrieve a deeply nested value. Never throws.
objx.get({ a: { b: [1, 2, 3] } }, "a.b[2]"); // 3
objx.get({ a: { b: null } }, "a.b.c", "default"); // "default"
objx.get(null, "a.b", "safe"); // "safe"- Supports dot notation:
"user.profile.name" - Supports bracket notation:
"users[0].email" - Supports mixed:
"data[2].rows[0].value" - Returns
fallback(orundefined) on missing/null paths
Set a deeply nested value, auto-creating missing objects and arrays.
// Mutable (default) — modifies obj in place
objx.set({}, "a.b.c", 42);
// { a: { b: { c: 42 } } }
// Immutable — returns a structurally-shared clone
const next = objx.set(state, "user.name", "Alice", { immutable: true });Options:
| Option | Type | Default | Description |
|---|---|---|---|
immutable |
boolean |
false |
Return new object instead of mutating |
Deep merge one or more source objects into target. Prototype-pollution safe.
objx.merge({ a: { x: 1 } }, { a: { y: 2 } });
// { a: { x: 1, y: 2 } }
// Concatenate arrays instead of replacing
objx.merge({ tags: ["a"] }, { tags: ["b"] }, { arrays: "concat" });
// { tags: ["a", "b"] }
// Deep-merge array elements
objx.merge(
{ items: [{ id: 1, x: 10 }] },
{ items: [{ id: 1, y: 20 }] },
{ arrays: "merge" }
);
// { items: [{ id: 1, x: 10, y: 20 }] }Options:
| Option | Type | Default | Description |
|---|---|---|---|
arrays |
"replace" | "concat" | "merge" |
"replace" |
Array merge strategy |
mutate |
boolean |
false |
Mutate target directly |
Check whether a nested path exists on an object.
objx.has({ a: { b: 1 } }, "a.b"); // true
objx.has({ a: { b: 1 } }, "a.c"); // false
objx.has({ a: null }, "a.b"); // falseDelete a deeply nested property. Returns true if deleted, false if not found.
const obj = { a: { b: 1, c: 2 } };
objx.del(obj, "a.b"); // true
// obj is now { a: { c: 2 } }Recursively fill in undefined values in target from source. Like _.defaults but deep.
objx.defaults(
{ db: { host: "localhost" } },
{ db: { host: "remote", port: 5432 }, timeout: 3000 }
);
// { db: { host: "localhost", port: 5432 }, timeout: 3000 }Pre-parse a path into a reusable getter. Eliminates path-parsing overhead in hot loops.
const getName = objx.compile("user.profile.name");
// In a tight loop — no string re-parsing on each call
users.map(getName); // fast!
// With fallback
getName(obj, "anonymous");import { get } from "@munesoft/objx";
app.post("/login", (req, res) => {
const userId = get(req, "body.user.id");
const authType = get(req, "headers.x-auth-type", "jwt");
const scopes = get(req, "body.scopes[0]", "read");
});import { defaults, get } from "@munesoft/objx";
const config = loadUserConfig();
const DEFAULTS = {
db: { host: "localhost", port: 5432, pool: 10 },
cache: { ttl: 300, max: 1000 },
logger: { level: "info" },
};
defaults(config, DEFAULTS);
const dbHost = get(config, "db.host");import { set, get } from "@munesoft/objx";
function reducer(state, action) {
switch (action.type) {
case "SET_USER_NAME":
return set(state, "user.name", action.payload, { immutable: true });
case "INCREMENT_SCORE":
const current = get(state, "game.score", 0);
return set(state, "game.score", current + 1, { immutable: true });
default:
return state;
}
}import { compile, merge } from "@munesoft/objx";
// Compile hot-path getters once
const getId = compile("data.id");
const getEmail = compile("contact.email");
const getName = compile("profile.name");
// Transform thousands of records efficiently
const normalized = rawRecords.map((rec) =>
merge({}, {
id: getId(rec),
email: getEmail(rec),
name: getName(rec, "Unknown"),
})
);objx blocks prototype-pollution attacks by design. The following keys are silently ignored across all operations:
__proto__constructorprototype
// This JSON payload cannot pollute Object.prototype
const malicious = JSON.parse('{"__proto__":{"isAdmin":true}}');
objx.merge({}, malicious);
({}).isAdmin; // undefined ✅Import only what you need:
import { get } from "@munesoft/objx/get";
import { set } from "@munesoft/objx/set";
import { merge } from "@munesoft/objx/merge";Full TypeScript support with strict types:
import { get, set, compile, merge } from "@munesoft/objx";
import type { MergeOptions, SetOptions } from "@munesoft/objx";
// Generic return types
const name = get<string>(obj, "user.name", "anon");
// ^? string
// Typed compiled getter
const getScore = compile<number>("game.score");
const score = getScore(state, 0);
// ^? number | undefinedobjx is optimised for speed:
- Path cache — paths are parsed once, then cached (LRU-capped at 512 entries).
compile()— for hot loops, bypass per-call parsing entirely.- Minimal allocations — immutable mode uses structural sharing.
- No dependencies — zero runtime overhead.
- lodash get alternative
- lodash set alternative
- lodash merge alternative
- deep object access javascript
- deep merge nodejs
- immutable object update js
- nested object path javascript
- safe nested access typescript
- object path utility npm
- prototype pollution safe merge
lodash · javascript · typescript · object-utils · deep-merge · immutable · deep-get · deep-set · lodash-alternative · npm
MIT © munesoft