@simpill/object.utils
v1.0.0
Published
Type-safe object utilities: pick, omit, merge, get/set by path, guards (Node and Edge).
Downloads
134
Maintainers
Readme
Features: Type-safe · Node.js & Edge · Tree-shakeable · Lightweight
Installation
From npm
npm install @simpill/object.utilsFrom GitHub
To use this package from the monorepo source:
git clone https://github.com/SkinnnyJay/simpill.git
cd simpill/utils/@simpill-object.utils
npm install && npm run buildIn your project you can then install from the local path:
npm install /path/to/simpill/utils/@simpill-object.utils
or use npm link from the package directory.
Usage
Subpath exports
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";Type utilities (type-only)
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>;Guards
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); // falseGet/Set by path
import { 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.
Pick / Omit
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" }Merge
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.
Immutable helpers
import { deepFreeze, deepSeal } from "@simpill/object.utils";
const state = deepFreeze({ user: { name: "Alice" } });
// state.user.name = "Bob"; // throws in strict modeCreate / define
import {
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.
Singleton
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();Bounded collections
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.
What we don’t provide
- 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.
API summary
| 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) |
Examples
npx ts-node examples/01-basic-usage.ts| Example | Description | |---------|-------------| | 01-basic-usage.ts | pick, omit, getByPath, getByPathOrDefault, setByPath, deepMerge, isPlainObject |
Development
npm run build
npm test
npm run test:coverage
npm run check:fix
npm run verifyDocumentation
- Examples: examples/ — run with
npx ts-node examples/01-basic-usage.ts. - Monorepo: CONTRIBUTING for creating and maintaining packages.
- README standard: Package README standard.
License
ISC
