[](https://www.npmjs.com/package/@simpill/object.utils) [](https://github.com/SkinnnyJay/simpill-utils/tree/main/utils/@simpill-object.utils)
npm
npm install @simpill/object.utilsGitHub (from monorepo)
git clone https://github.com/SkinnnyJay/simpill-utils.git && cd simpill-utils/utils/@simpill-object.utils && npm install && npm run buildThen in your project: npm install /path/to/simpill-utils/utils/@simpill-object.utils or npm link from that directory.
import { pick, omit, deepMerge, isPlainObject } from "@simpill/object.utils";
// or client/edge only
import { pick, deepFreeze } from "@simpill/object.utils/client";
// or server only
import { pick, deepFreeze } from "@simpill/object.utils/server";
// or shared only
import { pick, deepFreeze } from "@simpill/object.utils/shared";Use in type positions for safer object shapes:
import type {
PartialBy,
RequiredBy,
DeepPartial,
DeepRequired,
ValueOf,
KeysOfType,
StrictRecord,
} from "@simpill/object.utils";
type User = { id: number; name: string; email?: string };
type CreateUser = PartialBy<User, "id">; // id optional
type WithEmail = RequiredBy<User, "email">;
type PatchUser = DeepPartial<User>;import {
isPlainObject,
isObject,
isRecord,
isEmptyObject,
hasOwn,
isNil,
isDefined,
} from "@simpill/object.utils";
isPlainObject({}); // true
isPlainObject([]); // false
isEmptyObject({}); // true
hasOwn({ a: 1 }, "a"); // true
isDefined(null); // falseimport { getByPath, getByPathOrDefault, hasPath, setByPath } from "@simpill/object.utils";
const obj = { a: { b: { c: 1 } } };
getByPath(obj, "a.b.c"); // 1 (returns unknown; assert or narrow when you know the shape)
getByPath(obj, ""); // obj (empty path returns the object unchanged)
getByPathOrDefault(obj, "a.x", 0); // 0
hasPath(obj, "a.b"); // true
setByPath(obj, "a.b.d", 2); // mutates objThere is no deleteByPath. To remove a key at a path, use getByPath(obj, parentPath) to get the parent object, then delete parent[key] for the last segment, or implement a small helper that splits the path and mutates the parent.
Singleton: createSingleton(factory, key) stores instances on globalThis (one map per process). For process isolation this is fine; in tests call resetSingleton(key) or resetAllSingletons() to avoid leaking state between tests.
import { pick, omit, pickOne } from "@simpill/object.utils";
const user = { id: 1, name: "Alice", email: "[email protected]" };
pick(user, ["id", "name"]); // { id: 1, name: "Alice" }
omit(user, ["email"]); // { id: 1, name: "Alice" }
pickOne(user, "name"); // { name: "Alice" }import { shallowMerge, deepMerge } from "@simpill/object.utils";
shallowMerge({ a: 1, b: 2 }, { b: 3, c: 4 }); // { a: 1, b: 3, c: 4 }
deepMerge(
{ a: { b: 1, c: 2 } },
{ a: { c: 3, d: 4 } },
{ concatArrays: true, skipUndefined: true }
);
// { a: { b: 1, c: 3, d: 4 } }deepMerge options (DeepMergeOptions):
| Option | Default | Description |
|---|---|---|
concatArrays |
false |
If true, array values are concatenated (target then source); otherwise source array overwrites target. |
skipUndefined |
false |
If true, undefined in source does not overwrite existing target values. |
Deep merge only recurses into plain objects; other values (including arrays when not concatenating) are taken from source. Target is never mutated; a new object is returned. No cycle detection—do not pass objects with circular references or recursion may not terminate.
import { deepFreeze, deepSeal } from "@simpill/object.utils";
const state = deepFreeze({ user: { name: "Alice" } });
// state.user.name = "Bob"; // throws in strict modeimport {
createWithDefaults,
defineReadOnly,
fromEntries,
} from "@simpill/object.utils";
createWithDefaults({ a: 1, b: 2 }, { b: 3 }); // { a: 1, b: 3 }
const obj: Record<string, number> = {};
defineReadOnly(obj, "id", 42); // non-enumerable, read-only
fromEntries([["x", 1], ["y", 2]]); // { x: 1, y: 2 }Safe JSON stringify: This package does not provide a safe stringify helper. For "stringify or fallback" use toJsonSafe from @simpill/misc.utils, or JSON.stringify in a try/catch.
Single instance per key (uses globalThis; works with Next.js hot reload):
import { createSingleton, resetSingleton, resetAllSingletons } from "@simpill/object.utils";
const getCache = createSingleton(() => new Map<string, number>(), "my-cache");
const cache = getCache(); // same instance every time
// tests
resetSingleton("my-cache");
resetAllSingletons();BoundedLRUMap – size-limited Map with LRU eviction; optional logger for alerts:
import { BoundedLRUMap } from "@simpill/object.utils";
const cache = new BoundedLRUMap<string, number>(1000);
cache.set("k", 42);
cache.get("k"); // 42
// when full, oldest entry is evicted
// with optional logger (e.g. your app logger)
const withLogger = new BoundedLRUMap<string, number>({
maxSize: 100,
alertThreshold: 0.8,
logger: { warn: (msg, meta) => console.warn(msg, meta) },
});LRU behavior and complexity: get and set are O(1). On set, if the key already exists it is moved to "most recent"; if the map is full, the least-recently-used entry (oldest insertion/access order) is evicted. Eviction count is tracked and available via getStats(). For TTL-based caches or memoization, see @simpill/cache.utils.
BoundedArray – size-limited array with FIFO eviction; optional logger:
import { BoundedArray } from "@simpill/object.utils";
const buffer = new BoundedArray<string>(100);
buffer.push("a");
buffer.push("b");
// when over maxSize, oldest items are dropped
buffer.getStats(); // { size, maxSize, usagePercent, evictions }BoundedArray eviction: When length exceeds maxSize, the oldest items (front of the array) are dropped. getStats() reports evictions and usagePercent.
- Typed path helpers —
getByPathreturnsunknown; narrow with a type guard or generic wrapper at call site (e.g.getByPath(obj, "a.b") as numberor Zod parse).setByPathmutates. For type-safe paths, use a dedicated library or wrap with your own path tuple/union. - Deep equal / deep clone — No
deepEqualordeepClone. UsedeepClonefrom@simpill/data.utils, or lodashisEqual/cloneDeep. This package providesdeepMerge(merge only) anddeepFreeze/deepSeal. - Diff / patch — No structural diff or JSON Patch. Use a library (e.g.
fast-json-patch, lodash) if you need diff/patch. - Immutable “update by path” —
setByPathmutates. For immutable updates, clone first (e.g.deepClonefrom@simpill/data.utils) thensetByPathon the clone, or use spread/Immer. - Shape guards — We provide
isPlainObject,isRecord,isEmptyObject,hasOwn, etc. For “object has keysaandb” use Zod (or similar) or manualhasOwnchecks.
When to use: Pick/omit, path get/set, shallow/deep merge, type utilities, guards, singletons, and bounded in-memory structures. For full validation, deep clone, or diff/patch, combine with @simpill/data.utils or other libs.
| Category | Exports |
|---|---|
| Types | PartialBy, RequiredBy, DeepPartial, DeepRequired, ValueOf, Entries, KeysOfType, StrictRecord, Mutable, DeepReadonly, etc. |
| Guards | isPlainObject, isObject, isRecord, isEmptyObject, hasOwn, isNil, isDefined |
| Path | getByPath, getByPathOrDefault, hasPath, setByPath |
| Pick/Omit | pick, omit, pickOne |
| Merge | shallowMerge, deepMerge (with DeepMergeOptions) |
| Immutable | deepFreeze, deepSeal |
| Create | createWithDefaults, defineReadOnly, fromEntries |
| Singleton | createSingleton, resetSingleton, resetAllSingletons |
| Bounded | BoundedLRUMap, BoundedArray (optional logger in options) |
npx ts-node examples/01-basic-usage.ts| Example | Description |
|---|---|
| 01-basic-usage.ts | pick, omit, getByPath, getByPathOrDefault, setByPath, deepMerge, isPlainObject |
- CONTRIBUTING — Monorepo package guide.
ISC