any>(funcName: keyof T, clsProto?: T, polyFunc?: P) {
+ let clsFn = clsProto ? clsProto[funcName] : NULL_VALUE;
+
+ return function(thisArg: any): ReturnType {
+ let theFunc = clsFn;
+ if (theFunc || polyFunc) {
+ let theArgs = arguments;
+ return ((theFunc || polyFunc) as Function).apply(thisArg, theFunc ? ArrSlice[CALL](theArgs, 1) : theArgs);
+ }
+
+ throwTypeError("\"" + asString(funcName) + "\" not defined for " + dumpObj(thisArg));
+ };
+}
+
/**
* @internal
* @ignore
diff --git a/lib/src/object/copy.ts b/lib/src/object/copy.ts
index 672ffaf0..14bc8254 100644
--- a/lib/src/object/copy.ts
+++ b/lib/src/object/copy.ts
@@ -10,7 +10,7 @@ import { arrForEach } from "../array/forEach";
import { isArray, isDate, isNullOrUndefined, isPrimitiveType } from "../helpers/base";
import { CALL, FUNCTION, NULL_VALUE, OBJECT } from "../internal/constants";
import { objDefine } from "./define";
-import { forEachOwnKeySafe } from "./forEachOwnKeySafe";
+import { forEachOwnKeySafe } from "./forEachOwnKey";
import { isPlainObject } from "./is_plain_object";
import { isUnsafeTarget } from "./isUnsafeTarget";
diff --git a/lib/src/object/defaults.ts b/lib/src/object/defaults.ts
new file mode 100644
index 00000000..b40c231a
--- /dev/null
+++ b/lib/src/object/defaults.ts
@@ -0,0 +1,123 @@
+/*
+ * @nevware21/ts-utils
+ * https://github.com/nevware21/ts-utils
+ *
+ * Copyright (c) 2026 NevWare21 Solutions LLC
+ * Licensed under the MIT license.
+ */
+
+import { arrForEach } from "../array/forEach";
+import { isStrictUndefined } from "../helpers/base";
+import { forEachOwnKeySafe } from "./forEachOwnKey";
+import { isUnsafeTarget } from "./isUnsafeTarget";
+
+/**
+ * Copies own enumerable properties from `source` to `target` **only** when the predicate
+ * returns truthy for that key/value pair. All other own properties on `target` are left
+ * unchanged.
+ *
+ * Security behavior: this helper iterates source keys via {@link forEachOwnKeySafe}, so unsafe
+ * keys (`__proto__`, `constructor`, `prototype`) are ignored even if the predicate returns true.
+ * It also skips all writes when `target` is a guarded built-in prototype object per
+ * {@link isUnsafeTarget}.
+ * @since 0.14.0
+ * @group Object
+ * @typeParam T - The type of the target object
+ * @param target - The target object to merge into
+ * @param source - The source object to merge from. Null or undefined source is safely ignored.
+ * @param predicate - A function `(key, srcValue, tgtValue) => boolean` called for each own
+ * enumerable safe property key of `source` (string and symbol). The property is merged only
+ * when the predicate returns truthy.
+ * @returns The `target` object (mutated in place).
+ * @example
+ * ```ts
+ * const target = { a: 1, b: 2 };
+ * const source = { b: 99, c: 3 };
+ *
+ * // Only merge keys whose source value is greater than 2
+ * objMergeIf(target, source, (key, srcVal) => srcVal > 2);
+ * // target => { a: 1, b: 99, c: 3 }
+ *
+ * // Only merge when the key does not already exist in target
+ * const t2 = { x: 10 };
+ * objMergeIf(t2, { x: 99, y: 5 }, (key, _sv, tgtVal) => tgtVal === undefined);
+ * // t2 => { x: 10, y: 5 }
+ *
+ * // Unsafe keys are filtered even if predicate returns true
+ * const t3: any = {};
+ * objMergeIf(t3, { constructor: "ignored", safe: 1 } as any, () => true);
+ * // t3 => { safe: 1 }
+ * ```
+ */
+export function objMergeIf(
+ target: T,
+ source: Record | null | undefined,
+ predicate: (key: PropertyKey, srcValue: any, tgtValue: any) => boolean
+): T {
+ if (target && source && !isUnsafeTarget(target)) {
+ forEachOwnKeySafe(source, (key, value) => {
+ if (predicate(key, value, (target as any)[key])) {
+ (target as any)[key] = value;
+ }
+ });
+ }
+ return target;
+}
+
+/**
+ * Assigns own enumerable properties from one or more `sources` onto `target` **only** for
+ * properties that are currently `undefined` on `target` — it never overwrites an already-defined
+ * value (including `null`). Sources are processed left-to-right; the first defined value wins.
+ *
+ * This is similar to Lodash `_.defaults()`, but it only considers each source object's own
+ * enumerable properties and does not copy inherited source properties.
+ *
+ * **Security filtering:** to guard against prototype-pollution attacks this function applies two
+ * layers of protection:
+ * - **Unsafe source keys** (`__proto__`, `constructor`, `prototype`) are silently skipped and
+ * never written to `target`, even when those keys exist as own enumerable properties of a source.
+ * - **Guarded targets** (built-in prototype objects such as `Object.prototype`,
+ * `Array.prototype`, etc.) are rejected entirely — `target` is returned unchanged.
+ *
+ * This means that calls like `objDefaults(obj, { constructor: fn })` or
+ * `objDefaults(Object.prototype, ...)` are silently no-ops for the filtered keys / guarded target.
+ * @since 0.14.0
+ * @group Object
+ * @typeParam T - The type of the target object
+ * @param target - The destination object. Modified in place.
+ * @param sources - One or more source objects. Null / undefined sources are skipped.
+ * @returns The `target` object with all defaults applied.
+ * @example
+ * ```ts
+ * const options = { timeout: 5000 };
+ * const defaults = { timeout: 3000, retries: 3, verbose: false };
+ *
+ * objDefaults(options, defaults);
+ * // => { timeout: 5000, retries: 3, verbose: false }
+ * // `timeout` was kept because it was already defined.
+ *
+ * // Multiple sources — first defined value wins
+ * objDefaults({}, { a: 1 }, { a: 99, b: 2 });
+ * // => { a: 1, b: 2 }
+ *
+ * // Unsafe source keys are silently skipped (prototype-pollution guard)
+ * const cfg: any = { host: "localhost" };
+ * const src: any = { host: "evil.com", __proto__: { admin: true }, constructor: String };
+ * objDefaults(cfg, src);
+ * // => { host: "localhost" } — __proto__ and constructor were never written
+ * ```
+ */
+export function objDefaults(target: T, ...sources: Array | null | undefined>): T {
+ if (target && !isUnsafeTarget(target)) {
+ arrForEach(sources, (source) => {
+ if (source) {
+ forEachOwnKeySafe(source, (key, value) => {
+ if (isStrictUndefined((target as any)[key])) {
+ (target as any)[key] = value;
+ }
+ });
+ }
+ });
+ }
+ return target;
+}
diff --git a/lib/src/object/define.ts b/lib/src/object/define.ts
index af168150..aa1fd681 100644
--- a/lib/src/object/define.ts
+++ b/lib/src/object/define.ts
@@ -12,7 +12,7 @@ import { objForEachKey } from "./for_each_key";
import { ILazyValue } from "../helpers/lazy";
import { _pureAssign, _pureRef } from "../internal/treeshake_helpers";
import { arrForEach } from "../array/forEach";
-import { objPropertyIsEnumerable } from "./property_is_enumerable";
+import { _objPropertyIsEnumerable } from "./property_is_enumerable";
import { _returnEmptyArray, _returnNothing } from "../internal/stubs";
const _objGetOwnPropertyDescriptor: (target: any, prop: PropertyKey) => PropertyDescriptor | undefined = (/*#__PURE__*/_pureAssign((/*#__PURE__*/_pureRef(ObjClass as any, GET_OWN_PROPERTY_DESCRIPTOR)), _returnNothing));
@@ -268,7 +268,7 @@ export function objDefineProps(target: T, propDescMap: ObjDefinePropDescripto
});
arrForEach(_objGetOwnPropertySymbols(propDescMap), (sym) => {
- if (objPropertyIsEnumerable(propDescMap, sym)) {
+ if (_objPropertyIsEnumerable(propDescMap, sym)) {
props[sym] = _createProp(propDescMap[sym]);
}
});
diff --git a/lib/src/object/diff.ts b/lib/src/object/diff.ts
new file mode 100644
index 00000000..a3940f9f
--- /dev/null
+++ b/lib/src/object/diff.ts
@@ -0,0 +1,61 @@
+/*
+ * @nevware21/ts-utils
+ * https://github.com/nevware21/ts-utils
+ *
+ * Copyright (c) 2026 NevWare21 Solutions LLC
+ * Licensed under the MIT license.
+ */
+
+import { isStrictNullOrUndefined } from "../helpers/base";
+import { objCreate } from "./create";
+import { forEachOwnKey } from "./forEachOwnKey";
+import { objHasOwn } from "./has_own";
+
+/**
+ * Returns a shallow diff of two objects: a new object containing only the own enumerable
+ * properties from `modified` whose values differ from the corresponding values in `base`
+ * (using strict equality `!==`). Properties present in `modified` but not in `base` are
+ * also included. Properties removed in `modified` (i.e. present only in `base`) are
+ * **not** included — use the result to describe *what changed* in `modified`.
+ * @since 0.14.0
+ * @group Object
+ * @typeParam T - The type of the base object
+ * @typeParam U - The type of the modified object (defaults to `Partial`)
+ * @param base - The original / reference object
+ * @param modified - The updated object to compare against `base`
+ * @returns A new plain object with the keys from `modified` that differ from `base`.
+ * Returns an empty object when the two objects are equivalent or when either argument
+ * is null or undefined.
+ * @example
+ * ```ts
+ * const prev = { x: 1, y: 2, z: 3 };
+ * const next = { x: 1, y: 99, z: 3 };
+ *
+ * objDiff(prev, next); // { y: 99 }
+ *
+ * // Added keys are included
+ * objDiff({ a: 1 }, { a: 1, b: 2 }); // { b: 2 }
+ *
+ * // Removed keys are NOT included
+ * objDiff({ a: 1, b: 2 }, { a: 1 }); // {}
+ *
+ * // null / undefined values are compared strictly
+ * objDiff({ a: null }, { a: undefined }); // { a: undefined }
+ * ```
+ */
+/*#__NO_SIDE_EFFECTS__*/
+export function objDiff = Partial>(base: T, modified: U): Partial {
+ const result: Partial = objCreate(null);
+
+ if (!isStrictNullOrUndefined(base)) {
+ forEachOwnKey(modified, (key, value) => {
+ const hasBase = objHasOwn(base, key);
+ const baseVal = hasBase ? (base as any)[key] : undefined;
+ if (!hasBase || baseVal !== value) {
+ (result as any)[key] = value;
+ }
+ });
+ }
+
+ return result;
+}
diff --git a/lib/src/object/forEachOwnKey.ts b/lib/src/object/forEachOwnKey.ts
new file mode 100644
index 00000000..e5b0c558
--- /dev/null
+++ b/lib/src/object/forEachOwnKey.ts
@@ -0,0 +1,153 @@
+/*
+ * @nevware21/ts-utils
+ * https://github.com/nevware21/ts-utils
+ *
+ * Copyright (c) 2026 NevWare21 Solutions LLC
+ * Licensed under the MIT license.
+ */
+
+import { arrForEach } from "../array/forEach";
+import { isStrictNullOrUndefined } from "../helpers/base";
+import { CALL, NULL_VALUE, UNDEF_VALUE } from "../internal/constants";
+import { objGetOwnPropertySymbols } from "./get_own_property";
+import { objHasOwn } from "./has_own";
+import { isUnsafePropKey } from "./isUnsafePropKey";
+import { _objPropertyIsEnumerable } from "./property_is_enumerable";
+
+/**
+ * Calls the provided `callbackFn` once for each own enumerable key (string and symbol) in the
+ * supplied value. The callback can stop iteration early by returning `-1`.
+ *
+ * This helper works with plain objects, arrays, and functions while safely ignoring
+ * `null` and `undefined` values.
+ *
+ * **Note:** Unlike {@link objForEachKey}, this helper also iterates enumerable symbol keys via
+ * `Object.getOwnPropertySymbols`. Use {@link forEachOwnKeySafe} when iterating untrusted input
+ * to filter out unsafe keys like `__proto__`, `constructor`, and `prototype`.
+ * @since 0.14.0
+ * @group Object
+ * @typeParam T - The source type.
+ * @param theObject - The object-like value to iterate.
+ * @param callbackfn - Invoked for each own enumerable key.
+ * @param thisArg - [Optional] The `this` context for the callback. If omitted, `null`, or
+ * `undefined`, the iterated object is used as `this`.
+ * @example
+ * ```ts
+ * // Iterates both string and symbol keys
+ * const sym = Symbol("id");
+ * const obj = { name: "Alice", [sym]: 42 };
+ *
+ * forEachOwnKey(obj, (key, value) => {
+ * console.log(String(key), value);
+ * // "name" "Alice"
+ * // "Symbol(id)" 42
+ * });
+ * ```
+ * @example
+ * ```ts
+ * // Stop iteration early by returning -1
+ * const obj = { a: 1, b: 2, c: 3 };
+ * forEachOwnKey(obj, (key) => {
+ * console.log(key); // "a", "b"
+ * if (key === "b") {
+ * return -1; // stops here
+ * }
+ * });
+ * ```
+ * @example
+ * ```ts
+ * // Custom this context
+ * const obj = { value: 10 };
+ * const ctx = { multiplier: 3 };
+ * forEachOwnKey(obj, function(key, val) {
+ * console.log((val as number) * this.multiplier); // 30
+ * }, ctx);
+ * ```
+ */
+export function forEachOwnKey(theObject: T, callbackfn: (key: PropertyKey, value: T[keyof T]) => void | number, thisArg?: any): void {
+ if (theObject !== NULL_VALUE && theObject !== UNDEF_VALUE) {
+ for (const prop in theObject) {
+ if (objHasOwn(theObject, prop)) {
+ if (callbackfn[CALL](isStrictNullOrUndefined(thisArg) ? theObject : thisArg, prop, theObject[prop as keyof T]) === -1) {
+ return;
+ }
+ }
+ }
+
+ arrForEach(objGetOwnPropertySymbols(theObject), (key) => {
+ if (_objPropertyIsEnumerable(theObject, key)) {
+ if (callbackfn[CALL](isStrictNullOrUndefined(thisArg) ? theObject : thisArg, key, theObject[key as keyof T]) === -1) {
+ return -1;
+ }
+ }
+ });
+ }
+}
+
+/**
+ * Calls the provided `callbackFn` once for each own enumerable key (string and symbol) in the
+ * supplied value, skipping keys that are considered unsafe (`__proto__`, `constructor`,
+ * `prototype`). The callback can stop iteration early by returning `-1`.
+ *
+ * This helper wraps {@link forEachOwnKey} with extra key filtering for safer assignment flows.
+ * Use this instead of {@link forEachOwnKey} whenever keys come from untrusted input (e.g.
+ * user-supplied objects, parsed JSON).
+ * @since 0.14.0
+ * @group Object
+ * @typeParam T - The source type.
+ * @param theObject - The object-like value to iterate.
+ * @param callbackfn - Invoked for each safe own enumerable key.
+ * @param thisArg - [Optional] The `this` context for the callback. If omitted, `null`, or
+ * `undefined`, the iterated object is used as `this`.
+ * @example
+ * ```ts
+ * // Safely copy properties from an untrusted source
+ * function safeMerge(target: T, source: any): T {
+ * forEachOwnKeySafe(source, (key, value) => {
+ * (target as any)[key] = value;
+ * });
+ * return target;
+ * }
+ *
+ * // Note: use Object.defineProperty so "__proto__" is a real own enumerable
+ * // property rather than being treated as prototype syntax by the JS engine.
+ * const src: any = { name: "Alice" };
+ * Object.defineProperty(src, "__proto__", { value: "attack", enumerable: true, configurable: true, writable: true });
+ * const result = safeMerge({}, src);
+ * // result.name === "Alice" (__proto__ was silently skipped)
+ * ```
+ * @example
+ * ```ts
+ * // Symbol keys are included but unsafe string keys are filtered
+ * const sym = Symbol("safe");
+ * // Note: use Object.defineProperty so "__proto__" is a real own enumerable
+ * // property rather than being treated as prototype syntax by the JS engine.
+ * const obj: any = { a: 1, [sym]: "ok" };
+ * Object.defineProperty(obj, "__proto__", { value: "bad", enumerable: true, configurable: true, writable: true });
+ *
+ * forEachOwnKeySafe(obj, (key, value) => {
+ * console.log(String(key), value);
+ * // "a" 1
+ * // "Symbol(safe)" "ok"
+ * // "__proto__" is never visited
+ * });
+ * ```
+ * @example
+ * ```ts
+ * // Stop iteration early by returning -1
+ * const obj = { a: 1, b: 2, c: 3 };
+ * forEachOwnKeySafe(obj, (key) => {
+ * console.log(key); // "a", "b"
+ * if (key === "b") {
+ * return -1; // stops here
+ * }
+ * });
+ * ```
+ */
+export function forEachOwnKeySafe(theObject: T, callbackfn: (key: PropertyKey, value: T[keyof T]) => void | number, thisArg?: any): void {
+ forEachOwnKey(theObject, (key, value) => {
+ if (!isUnsafePropKey(key)) {
+ return callbackfn[CALL](isStrictNullOrUndefined(thisArg) ? theObject : thisArg, key, value);
+ }
+ }, thisArg);
+}
diff --git a/lib/src/object/forEachOwnKeySafe.ts b/lib/src/object/forEachOwnKeySafe.ts
deleted file mode 100644
index de33f622..00000000
--- a/lib/src/object/forEachOwnKeySafe.ts
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * @nevware21/ts-utils
- * https://github.com/nevware21/ts-utils
- *
- * Copyright (c) 2026 NevWare21 Solutions LLC
- * Licensed under the MIT license.
- */
-
-import { CALL, NULL_VALUE, UNDEF_VALUE } from "../internal/constants";
-import { objHasOwn } from "./has_own";
-import { isUnsafePropKey } from "./isUnsafePropKey";
-
-/**
- * Calls the provided `callbackFn` once for each own enumerable key in the supplied value,
- * skipping keys that are considered unsafe (`__proto__`, `constructor`, `prototype`).
- * The callback can stop iteration early by returning `-1`.
- *
- * This helper works with plain objects, arrays, and functions while safely ignoring
- * `null` and `undefined` values.
- * @since 0.14.0
- * @group Object
- * @typeParam T - The source type.
- * @param theObject - The object-like value to iterate.
- * @param callbackfn - Invoked for each safe own enumerable key.
- * @param thisArg - [Optional] The `this` context for the callback.
- * @example
- * ```ts
- * const result: string[] = [];
- * forEachOwnKeySafe({ a: 1, constructor: 2, b: 3 }, (key) => {
- * result.push(key);
- * });
- *
- * // result === ["a", "b"]
- * ```
- */
-export function forEachOwnKeySafe(theObject: T, callbackfn: (key: string, value: T[keyof T]) => void | number, thisArg?: any): void {
- if (theObject !== NULL_VALUE && theObject !== UNDEF_VALUE) {
- for (const prop in theObject) {
- if (objHasOwn(theObject, prop) && !isUnsafePropKey(prop)) {
- if (callbackfn[CALL](thisArg || theObject, prop, theObject[prop as keyof T]) === -1) {
- break;
- }
- }
- }
- }
-}
diff --git a/lib/src/object/for_each_key.ts b/lib/src/object/for_each_key.ts
index 19e86281..c015b37f 100644
--- a/lib/src/object/for_each_key.ts
+++ b/lib/src/object/for_each_key.ts
@@ -9,34 +9,54 @@
import { isFunction, isObject, isStrictNullOrUndefined } from "../helpers/base";
import { CALL } from "../internal/constants";
import { objHasOwn } from "./has_own";
+import { isUnsafePropKey } from "./isUnsafePropKey";
/**
- * Calls the provided `callbackFn` function once for each key in an object. This is equivelent to `arrForEach(Object.keys(theObject), callbackFn)` or
- * if not using the array helper `Object.keys(theObject).forEach(callbackFn)` except that this helper avoid creating a temporary of the object
+ * Calls the provided `callbackFn` function once for each key in an object. This is equivalent to `arrForEach(Object.keys(theObject), callbackFn)` or
+ * if not using the array helper `Object.keys(theObject).forEach(callbackFn)` except that this helper avoids creating a temporary array of the object
* keys before iterating over them and like the `arrForEach` helper you CAN stop or break the iteration by returning -1 from the `callbackFn` function.
*
+ * **Note:** This helper only iterates string (and numeric) keys (via for...in loop) and does NOT include enumerable symbol keys.
+ * If you need to iterate all keys including symbol, use {@link forEachOwnKey} or {@link forEachOwnKeySafe} instead.
+ *
* Caution: this helper does not filter unsafe keys like `__proto__`, `constructor`, or `prototype`.
* If your callback uses the returned key to assign directly to another object (for example
* `target[key] = value`), validate keys first (for example with {@link isUnsafePropKey}) or
- * use {@link forEachOwnKeySafe} when iterating untrusted input.
+ * use {@link objForEachKeySafe} when iterating untrusted input.
* @group Object
* @typeParam T - The object type
+ * @param theObject - The object to iterate over
* @param callbackfn - A function that accepts up to two arguments, the key name and the current value of the property represented by the key.
* @param thisArg - [Optional] An object to which the this keyword can refer in the callbackfn function. If thisArg is omitted, null or undefined
* the object will be used as the this value.
* @example
* ```ts
- * function performAction(target: T, source: any) {
- * if (!isNullOrUndefined(source)) {
- * objForEachKey(source, (key, value) => {
- * // Set the target with a reference to the same value with the same name
- * target[key] = value;
- * });
- * }
- *
- * return target;
- * }
+ * // Basic iteration - only string keys
+ * const obj = { name: "Alice", age: 30 };
+ * objForEachKey(obj, (key, value) => {
+ * console.log(key, value); // name Alice, age 30
+ * });
* ```
+ * @example
+ * ```ts
+ * // Unsafe keys warning - use objForEachKeySafe for untrusted input
+ * // Note: use Object.defineProperty (not an object literal) so "__proto__" is
+ * // created as a normal own enumerable property rather than being treated
+ * // specially by the JS engine.
+ * const untrustedObj: any = { name: "Bob" };
+ * Object.defineProperty(untrustedObj, "__proto__", { value: "attack", enumerable: true, configurable: true, writable: true });
+ * objForEachKey(untrustedObj, (key, value) => {
+ * console.log(key, value); // name Bob, __proto__ attack (UNSAFE!)
+ * });
+ *
+ * // Safe iteration with objForEachKeySafe
+ * objForEachKeySafe(untrustedObj, (key, value) => {
+ * console.log(key, value); // name Bob (unsafe keys filtered)
+ * });
+ * ```
+ * @see {@link objForEachKeySafe} for safe iteration that filters unsafe keys
+ * @see {@link forEachOwnKey} for iteration that includes both string and symbol keys
+ * @see {@link forEachOwnKeySafe} for safe iteration with both string and symbol keys
*/
export function objForEachKey(theObject: T, callbackfn: (key: string, value: T[keyof T]) => void | number, thisArg?: any): void {
if (theObject && (isObject(theObject) || isFunction(theObject))) {
@@ -49,3 +69,54 @@ export function objForEachKey(theObject: T, callbackfn: (key: string, value:
}
}
}
+
+/**
+ * Calls the provided `callbackFn` function once for each key in an object, filtering out unsafe keys like `__proto__`, `constructor`, and `prototype`.
+ * This is a safe wrapper around {@link objForEachKey} that validates keys before passing them to the callback.
+ *
+ * Like {@link objForEachKey}, this helper iterates only string keys (via for...in loop) and does NOT include enumerable symbol keys.
+ * If you need to iterate both string and symbol keys, use {@link forEachOwnKeySafe} instead.
+ * @group Object
+ * @since 0.14.0
+ * @typeParam T - The object type
+ * @param theObject - The object to iterate over
+ * @param callbackfn - A function that accepts up to two arguments, the key name and the current value of the property represented by the key.
+ * @param thisArg - [Optional] An object to which the this keyword can refer in the callbackfn function. If thisArg is omitted, null or undefined
+ * the object will be used as the this value.
+ * @example
+ * ```ts
+ * // Safe iteration - unsafe keys are skipped
+ * const untrustedObj: any = { name: "Alice", constructor: {} };
+ * Object.defineProperty(untrustedObj, "__proto__", { value: "attack", enumerable: true, configurable: true, writable: true });
+ * objForEachKeySafe(untrustedObj, (key, value) => {
+ * console.log(key, value); // Only prints: name Alice
+ * });
+ * ```
+ * @example
+ * ```ts
+ * // Difference between objForEachKey variants:
+ * const source = { name: "Alice", age: 30, [Symbol.for("id")]: 123 };
+ *
+ * // objForEachKey - includes unsafe keys if present, no symbol keys
+ * objForEachKey(source, (key, value) => {
+ * console.log(key, value); // name Alice, age 30
+ * });
+ *
+ * // objForEachKeySafe - filters unsafe keys, no symbol keys
+ * objForEachKeySafe(source, (key, value) => {
+ * console.log(key, value); // name Alice, age 30 (same as above since no unsafe keys)
+ * });
+ *
+ * // forEachOwnKeySafe - filters unsafe keys, includes symbol keys
+ * forEachOwnKeySafe(source, (key, value) => {
+ * console.log(key, value); // name Alice, age 30, Symbol(id) 123
+ * });
+ * ```
+ */
+export function objForEachKeySafe(theObject: T, callbackfn: (key: string, value: T[keyof T]) => void | number, thisArg?: any): void {
+ objForEachKey(theObject, (key: string, value: T[keyof T]) => {
+ if (!isUnsafePropKey(key)) {
+ return callbackfn[CALL](isStrictNullOrUndefined(thisArg) ? theObject : thisArg, key, value);
+ }
+ }, thisArg);
+}
diff --git a/lib/src/object/isUnsafeTarget.ts b/lib/src/object/isUnsafeTarget.ts
index 15a89a02..fedd70a6 100644
--- a/lib/src/object/isUnsafeTarget.ts
+++ b/lib/src/object/isUnsafeTarget.ts
@@ -58,6 +58,22 @@ function _getUnsafeTargets(): any[] {
_addTargetProto(targets, safe(getInst as any, ["WeakSet"]).v);
_addTargetProto(targets, safe(getInst as any, ["Promise"]).v);
_addTargetProto(targets, safe(getInst as any, ["Symbol"]).v);
+ _addTargetProto(targets, safe(getInst as any, ["ArrayBuffer"]).v);
+ _addTargetProto(targets, safe(getInst as any, ["SharedArrayBuffer"]).v);
+ _addTargetProto(targets, safe(getInst as any, ["DataView"]).v);
+ _addTargetProto(targets, safe(getInst as any, ["Int8Array"]).v);
+ _addTargetProto(targets, safe(getInst as any, ["Uint8Array"]).v);
+ _addTargetProto(targets, safe(getInst as any, ["Uint8ClampedArray"]).v);
+ _addTargetProto(targets, safe(getInst as any, ["Int16Array"]).v);
+ _addTargetProto(targets, safe(getInst as any, ["Uint16Array"]).v);
+ _addTargetProto(targets, safe(getInst as any, ["Int32Array"]).v);
+ _addTargetProto(targets, safe(getInst as any, ["Uint32Array"]).v);
+ _addTargetProto(targets, safe(getInst as any, ["Float32Array"]).v);
+ _addTargetProto(targets, safe(getInst as any, ["Float64Array"]).v);
+ _addTargetProto(targets, safe(getInst as any, ["BigInt64Array"]).v);
+ _addTargetProto(targets, safe(getInst as any, ["BigUint64Array"]).v);
+ _addTargetProto(targets, safe(getInst as any, ["WeakRef"]).v);
+ _addTargetProto(targets, safe(getInst as any, ["FinalizationRegistry"]).v);
return targets;
}
diff --git a/lib/src/object/map_values.ts b/lib/src/object/map_values.ts
new file mode 100644
index 00000000..e549b2bf
--- /dev/null
+++ b/lib/src/object/map_values.ts
@@ -0,0 +1,42 @@
+/*
+ * @nevware21/ts-utils
+ * https://github.com/nevware21/ts-utils
+ *
+ * Copyright (c) 2026 NevWare21 Solutions LLC
+ * Licensed under the MIT license.
+ */
+
+import { objCreate } from "./create";
+import { forEachOwnKey } from "./forEachOwnKey";
+
+/**
+ * Creates a new object with the same keys as `source` but with each value transformed
+ * by the provided `mapper` function.
+ * @since 0.14.0
+ * @group Object
+ * @typeParam T - The type of the source object
+ * @typeParam U - The type of the mapped values
+ * @param source - The source object whose values will be mapped
+ * @param mapper - A function `(value, key) => U` called for every own enumerable property.
+ * @returns A new object with the same keys as `source` and values produced by `mapper`.
+ * Returns an empty object if `source` is null or undefined.
+ * @example
+ * ```ts
+ * const prices = { apple: 1.5, banana: 0.75, cherry: 3.0 };
+ *
+ * objMapValues(prices, (v) => v * 2);
+ * // => { apple: 3, banana: 1.5, cherry: 6 }
+ *
+ * const user = { firstName: "ada", lastName: "lovelace" };
+ * objMapValues(user, (v) => v.toUpperCase());
+ * // => { firstName: "ADA", lastName: "LOVELACE" }
+ * ```
+ */
+/*#__NO_SIDE_EFFECTS__*/
+export function objMapValues(source: T, mapper: (value: T[keyof T], key: PropertyKey) => U): { [K in keyof T]: U } {
+ const result: any = objCreate(null);
+ forEachOwnKey(source, (key, value) => {
+ result[key] = mapper(value, key);
+ });
+ return result;
+}
diff --git a/lib/src/object/pick.ts b/lib/src/object/pick.ts
new file mode 100644
index 00000000..9e68bfa5
--- /dev/null
+++ b/lib/src/object/pick.ts
@@ -0,0 +1,170 @@
+/*
+ * @nevware21/ts-utils
+ * https://github.com/nevware21/ts-utils
+ *
+ * Copyright (c) 2026 NevWare21 Solutions LLC
+ * Licensed under the MIT license.
+ */
+
+import { arrForEach } from "../array/forEach";
+import { arrIndexOf } from "../array/indexOf";
+import { isArrayLike, isString } from "../helpers/base";
+import { objCreate } from "./create";
+import { forEachOwnKey } from "./forEachOwnKey";
+import { objHasOwn } from "./has_own";
+import { _objPropertyIsEnumerable } from "./property_is_enumerable";
+
+/**
+ * Creates a new object composed of the picked enumerable own properties of `source`.
+ * Only keys present in the `keys` array are included in the returned object.
+ * @since 0.14.0
+ * @group Object
+ * @typeParam T - The type of the source object
+ * @typeParam K - The key type (subset of keyof T)
+ * @param source - The source object to pick from
+ * @param keys - The array of keys to include
+ * @returns A new object containing only the specified keys and their values from `source`.
+ * Returns an empty object if `source` is null or undefined.
+ * @example
+ * ```ts
+ * const obj = { a: 1, b: "hello", c: true };
+ *
+ * objPick(obj, ["a", "c"]); // { a: 1, c: true }
+ * objPick(obj, ["b"]); // { b: "hello" }
+ * objPick(obj, []); // {}
+ * objPick(null, ["a"]); // {}
+ * ```
+ */
+/*#__NO_SIDE_EFFECTS__*/
+export function objPick(source: T, keys: ArrayLike): Pick;
+/*#__NO_SIDE_EFFECTS__*/
+export function objPick(source: T, keys: ArrayLike): Partial;
+/*#__NO_SIDE_EFFECTS__*/
+export function objPick(source: T, keys: ArrayLike): Partial;
+export function objPick(source: T, keys: ArrayLike): Partial {
+ const result: Partial = objCreate(null);
+ if (source && isArrayLike(keys)) {
+ arrForEach(keys, (key) => {
+ if (objHasOwn(source, key) && _objPropertyIsEnumerable(source, key)) {
+ (result as any)[key] = (source as any)[key];
+ }
+ });
+ }
+ return result;
+}
+
+/**
+ * Creates a new object composed of the enumerable own properties of `source` **except** for
+ * those matching the provided `keys`.
+ * @since 0.14.0
+ * @group Object
+ * @typeParam T - The type of the source object
+ * @typeParam K - The key type (subset of keyof T) to exclude
+ * @param source - The source object to omit from
+ * @param keys - The array of keys to exclude
+ * @returns A new object containing all own enumerable properties of `source` except those listed in `keys`.
+ * Returns an empty object if `source` is null or undefined.
+ * @example
+ * ```ts
+ * const obj = { a: 1, b: "hello", c: true };
+ *
+ * objOmit(obj, ["b"]); // { a: 1, c: true }
+ * objOmit(obj, ["a", "c"]); // { b: "hello" }
+ * objOmit(obj, []); // { a: 1, b: "hello", c: true }
+ * objOmit(null, ["a"]); // {}
+ * ```
+ */
+/*#__NO_SIDE_EFFECTS__*/
+export function objOmit(source: T, keys: ArrayLike): Pick>;
+/*#__NO_SIDE_EFFECTS__*/
+export function objOmit(source: T, keys: ArrayLike): Partial;
+/*#__NO_SIDE_EFFECTS__*/
+export function objOmit(source: T, keys: ArrayLike): Partial;
+export function objOmit(source: T, keys: ArrayLike): Partial {
+ const result: Partial = objCreate(null);
+ if (source && isArrayLike(keys)) {
+ forEachOwnKey(source, (key, value) => {
+ let hasKey = arrIndexOf(keys, key) !== -1;
+ if (!hasKey && isString(key)) {
+ // Also check for numeric keys using their runtime property names (e.g. "1" for key 1)
+ const numericKey = +key;
+ if (key === numericKey + "") {
+ if (numericKey === numericKey) {
+ hasKey = arrIndexOf(keys, numericKey) !== -1;
+ } else {
+ arrForEach(keys, (candidate) => {
+ if (candidate !== candidate) {
+ hasKey = true;
+ }
+ });
+ }
+ }
+ }
+
+ if (!hasKey) {
+ (result as any)[key] = value;
+ }
+ });
+ }
+ return result;
+}
+
+/**
+ * Creates a new object composed of the enumerable own properties of `source` for which
+ * the predicate function returns a truthy value.
+ * @since 0.14.0
+ * @group Object
+ * @typeParam T - The type of the source object
+ * @param source - The source object to pick from
+ * @param predicate - A function `(key, value) => boolean` that is invoked for each own enumerable
+ * property. A property is included in the result when the predicate returns truthy.
+ * @returns A new object containing only the properties for which `predicate` returned truthy.
+ * Returns an empty object if `source` is null or undefined.
+ * @example
+ * ```ts
+ * const obj = { a: 1, b: 2, c: 3, d: 4 };
+ *
+ * objPickBy(obj, (key, value) => value > 2); // { c: 3, d: 4 }
+ * objPickBy(obj, (key) => key !== "b"); // { a: 1, c: 3, d: 4 }
+ * ```
+ */
+/*#__NO_SIDE_EFFECTS__*/
+export function objPickBy(source: T, predicate: (key: PropertyKey, value: T[keyof T]) => boolean): Partial {
+ const result: Partial = objCreate(null);
+ forEachOwnKey(source, (key, value) => {
+ if (predicate(key, value)) {
+ (result as any)[key] = value;
+ }
+ });
+ return result;
+}
+
+/**
+ * Creates a new object composed of the enumerable own properties of `source` for which
+ * the predicate function returns a **falsy** value — the inverse of {@link objPickBy}.
+ * @since 0.14.0
+ * @group Object
+ * @typeParam T - The type of the source object
+ * @param source - The source object to omit from
+ * @param predicate - A function `(key, value) => boolean` that is invoked for each own enumerable
+ * property. A property is **excluded** from the result when the predicate returns truthy.
+ * @returns A new object containing only the properties for which `predicate` returned falsy.
+ * Returns an empty object if `source` is null or undefined.
+ * @example
+ * ```ts
+ * const obj = { a: 1, b: 2, c: 3, d: 4 };
+ *
+ * objOmitBy(obj, (key, value) => value > 2); // { a: 1, b: 2 }
+ * objOmitBy(obj, (key) => key === "b"); // { a: 1, c: 3, d: 4 }
+ * ```
+ */
+/*#__NO_SIDE_EFFECTS__*/
+export function objOmitBy(source: T, predicate: (key: PropertyKey, value: T[keyof T]) => boolean): Partial {
+ const result: Partial = objCreate(null);
+ forEachOwnKey(source, (key, value) => {
+ if (!predicate(key, value)) {
+ (result as any)[key] = value;
+ }
+ });
+ return result;
+}
diff --git a/lib/src/object/property_is_enumerable.ts b/lib/src/object/property_is_enumerable.ts
index 8ecc1801..e38f7a85 100644
--- a/lib/src/object/property_is_enumerable.ts
+++ b/lib/src/object/property_is_enumerable.ts
@@ -8,11 +8,12 @@
import { isStrictNullOrUndefined } from "../helpers/base";
import { safe } from "../helpers/safe";
-import { NULL_VALUE, ObjClass } from "../internal/constants";
-import { _unwrapFunctionWithPoly } from "../internal/unwrapFunction";
+import { NULL_VALUE, ObjClass, ObjProto } from "../internal/constants";
+import { _unwrapFunctionNoInstWithPoly, _unwrapFunctionWithPoly } from "../internal/unwrapFunction";
+import { objHasOwnProperty } from "./has_own_prop";
function _objPropertyIsEnum(obj: any, propKey: PropertyKey): boolean {
- let desc: PropertyDescriptor | undefined;
+ let desc: PropertyDescriptor | null | undefined;
let fn = ObjClass.getOwnPropertyDescriptor;
if (!isStrictNullOrUndefined(obj) && fn) {
@@ -20,13 +21,13 @@ function _objPropertyIsEnum(obj: any, propKey: PropertyKey): boolean {
desc = safe(fn, [obj, propKey]).v || NULL_VALUE;
}
- if (!desc) {
+ if (!desc && !isStrictNullOrUndefined(obj)) {
desc = safe(() => {
// Check enumerability of the property in the object
// This is a workaround for the fact that `in` operator does not check for non-enumerable properties
for (const key in obj) {
- if (key === propKey) {
- return { enumerable: true };
+ if (key == propKey) {
+ return { enumerable: objHasOwnProperty(obj, key) } as PropertyDescriptor;
}
}
}).v;
@@ -67,3 +68,21 @@ function _objPropertyIsEnum(obj: any, propKey: PropertyKey): boolean {
* ```
*/
export const objPropertyIsEnumerable: (obj: any, prop: PropertyKey) => boolean = (/*#__PURE__*/_unwrapFunctionWithPoly("propertyIsEnumerable", NULL_VALUE as any, _objPropertyIsEnum));
+
+/**
+ * @internal
+ * The `_objPropertyIsEnumerable()` method returns a Boolean indicating whether the specified property
+ * is enumerable and is a property of the specified object. This method is similar to the native
+ * `Object.prototype.propertyIsEnumerable()` method, but it is a standalone function that can be used
+ * without needing to call it on the object itself. This uses the native
+ * `Object.prototype.propertyIsEnumerable()` implementation when it is available on the prototype
+ * captured at module initialization time; otherwise it falls back to the helper polyfill implementation.
+ *
+ * @function
+ * @since 0.14.0
+ * @group Object
+ * @param obj - The object on which to check if the property is enumerable
+ * @param prop - The property name or symbol to check
+ * @returns A Boolean indicating whether the specified property is enumerable
+ */
+export const _objPropertyIsEnumerable: (obj: any, prop: PropertyKey) => boolean = (/*#__PURE__*/_unwrapFunctionNoInstWithPoly("propertyIsEnumerable", ObjProto, _objPropertyIsEnum));
\ No newline at end of file
diff --git a/lib/test/bundle-size-check.js b/lib/test/bundle-size-check.js
index 51860c44..61699374 100644
--- a/lib/test/bundle-size-check.js
+++ b/lib/test/bundle-size-check.js
@@ -7,25 +7,25 @@ const configs = [
{
name: "es5-min-full",
path: "../bundle/es5/umd/ts-utils.min.js",
- limit: 34 * 1024, // 34 kb in bytes
+ limit: 36 * 1024, // 36 kb in bytes
compress: false
},
{
name: "es6-min-full",
path: "../bundle/es6/umd/ts-utils.min.js",
- limit: 33.5 * 1024, // 33.5 kb in bytes
+ limit: 35.5 * 1024, // 35.5 kb in bytes
compress: false
},
{
name: "es5-min-zip",
path: "../bundle/es5/umd/ts-utils.min.js",
- limit: 13.5 * 1024, // 13.5 kb in bytes
+ limit: 14 * 1024, // 14 kb in bytes
compress: true
},
{
name: "es6-min-zip",
path: "../bundle/es6/umd/ts-utils.min.js",
- limit: 13.5 * 1024, // 13.5 kb in bytes
+ limit: 14 * 1024, // 14 kb in bytes
compress: true
},
{
diff --git a/lib/test/src/common/object/defaults.test.ts b/lib/test/src/common/object/defaults.test.ts
new file mode 100644
index 00000000..7c8cab4c
--- /dev/null
+++ b/lib/test/src/common/object/defaults.test.ts
@@ -0,0 +1,316 @@
+/*
+ * @nevware21/ts-utils
+ * https://github.com/nevware21/ts-utils
+ *
+ * Copyright (c) 2026 NevWare21 Solutions LLC
+ * Licensed under the MIT license.
+ */
+
+import { assert } from "@nevware21/tripwire-chai";
+import { objMergeIf, objDefaults } from "../../../../src/object/defaults";
+import { hasSymbol } from "../../../../src/symbol/symbol";
+
+describe("object defaults utilities", () => {
+
+ // ─── objMergeIf ─────────────────────────────────────────────────────────────
+
+ describe("objMergeIf", () => {
+ it("should merge properties when predicate returns true", () => {
+ const target: any = { a: 1, b: 2 };
+ const source = { b: 99, c: 3 };
+ objMergeIf(target, source, () => true);
+ assert.deepEqual(target, { a: 1, b: 99, c: 3 });
+ });
+
+ it("should not merge properties when predicate returns false", () => {
+ const target = { a: 1, b: 2 };
+ const source = { b: 99, c: 3 };
+ objMergeIf(target, source, () => false);
+ assert.deepEqual(target, { a: 1, b: 2 });
+ });
+
+ it("should merge only selected properties by value", () => {
+ const target: any = { a: 1, b: 2 };
+ const source = { b: 99, c: 3 };
+ objMergeIf(target, source, (_k, srcVal) => srcVal > 2);
+ assert.equal(target.b, 99);
+ assert.equal(target.c, 3);
+ assert.equal(target.a, 1);
+ });
+
+ it("should pass srcValue and tgtValue correctly to predicate", () => {
+ const target: any = { a: 10 };
+ const source: any = { a: 20 };
+ let receivedSrc: any;
+ let receivedTgt: any;
+ objMergeIf(target, source, (_k, sv, tv) => {
+ receivedSrc = sv;
+ receivedTgt = tv;
+ return true;
+ });
+ assert.equal(receivedSrc, 20);
+ assert.equal(receivedTgt, 10);
+ });
+
+ it("should return the target object", () => {
+ const target: any = { a: 1 };
+ const result = objMergeIf(target, { b: 2 }, () => true);
+ assert.equal(result, target);
+ });
+
+ it("should safely ignore null source", () => {
+ const target = { a: 1 };
+ objMergeIf(target, null, () => true);
+ assert.deepEqual(target, { a: 1 });
+ });
+
+ it("should safely ignore undefined source", () => {
+ const target = { a: 1 };
+ objMergeIf(target, undefined, () => true);
+ assert.deepEqual(target, { a: 1 });
+ });
+
+ it("should use predicate key argument correctly", () => {
+ const target: any = { a: 1, b: 2 };
+ const source: any = { a: 100, b: 200 };
+ objMergeIf(target, source, (k) => k === "b");
+ assert.equal(target.a, 1);
+ assert.equal(target.b, 200);
+ });
+
+ it("should support merging when target property is undefined", () => {
+ const target: any = { a: 1 };
+ const source: any = { b: 2 };
+ objMergeIf(target, source, (_k, _sv, tv) => tv === undefined);
+ assert.equal(target.b, 2);
+ assert.equal(target.a, 1);
+ });
+
+ it("should not process inherited source properties", () => {
+ const proto = { inherited: 42 };
+ const source: any = Object.create(proto);
+ source.own = 99;
+ const target: any = {};
+ objMergeIf(target, source, () => true);
+ assert.equal(target.own, 99);
+ assert.isFalse("inherited" in target);
+ });
+
+ it("should ignore unsafe source keys", () => {
+ const target: any = {};
+ const source: any = {};
+ Object.defineProperty(source, "__proto__", {
+ value: { polluted: true },
+ enumerable: true,
+ configurable: true,
+ writable: true
+ });
+ source.constructor = "bad";
+ source.prototype = "bad";
+ source.safe = 1;
+ objMergeIf(target, source, () => true);
+ assert.equal(target.safe, 1);
+ assert.isFalse(Object.prototype.hasOwnProperty.call(target, "__proto__"));
+ assert.isFalse(Object.prototype.hasOwnProperty.call(target, "constructor"));
+ assert.isFalse(Object.prototype.hasOwnProperty.call(target, "prototype"));
+ assert.isUndefined((target as any).polluted);
+ assert.isUndefined(({} as any).polluted);
+ });
+
+ it("should not write through unsafe targets", () => {
+ const original = (Object.prototype as any).polluted;
+ try {
+ objMergeIf(Object.prototype as any, { polluted: "bad" }, () => true);
+ assert.equal((Object.prototype as any).polluted, original);
+ } finally {
+ if (original === undefined) {
+ delete (Object.prototype as any).polluted;
+ } else {
+ (Object.prototype as any).polluted = original;
+ }
+ }
+ });
+
+ it("should merge enumerable symbol keyed properties", () => {
+ if (!hasSymbol()) {
+ return;
+ }
+
+ const sym = Symbol("mergeIf");
+ const target: any = {};
+ const source: any = {};
+ source[sym] = 42;
+
+ objMergeIf(target, source, () => true);
+
+ assert.equal(target[sym], 42);
+ });
+ });
+
+ // ─── objDefaults ────────────────────────────────────────────────────────────
+
+ describe("objDefaults", () => {
+ it("should assign properties not present on target", () => {
+ const target: any = { a: 1 };
+ objDefaults(target, { b: 2, c: 3 });
+ assert.deepEqual(target, { a: 1, b: 2, c: 3 });
+ });
+
+ it("should not overwrite properties already defined on target", () => {
+ const target: any = { a: 1, b: 5 };
+ objDefaults(target, { a: 99, b: 99, c: 3 });
+ assert.equal(target.a, 1);
+ assert.equal(target.b, 5);
+ assert.equal(target.c, 3);
+ });
+
+ it("should fill in undefined values on target", () => {
+ const target: any = { a: undefined };
+ objDefaults(target, { a: 42 });
+ assert.equal(target.a, 42);
+ });
+
+ it("should NOT fill in null values (null is defined)", () => {
+ const target: any = { a: null };
+ objDefaults(target, { a: 42 });
+ assert.isNull(target.a, "null is a defined value and should not be replaced");
+ });
+
+ it("should handle multiple sources left-to-right, first wins", () => {
+ const result: any = {};
+ objDefaults(result, { a: 1 }, { a: 99, b: 2 }, { b: 99, c: 3 });
+ assert.equal(result.a, 1);
+ assert.equal(result.b, 2);
+ assert.equal(result.c, 3);
+ });
+
+ it("should skip null sources", () => {
+ const target: any = {};
+ objDefaults(target, null, { a: 1 });
+ assert.equal(target.a, 1);
+ });
+
+ it("should skip undefined sources", () => {
+ const target: any = {};
+ objDefaults(target, undefined, { a: 1 });
+ assert.equal(target.a, 1);
+ });
+
+ it("should return the target object", () => {
+ const target: any = { a: 1 };
+ const result = objDefaults(target, { b: 2 });
+ assert.equal(result, target);
+ });
+
+ it("should work with zero sources", () => {
+ const target: any = { a: 1 };
+ objDefaults(target);
+ assert.deepEqual(target, { a: 1 });
+ });
+
+ it("should not include inherited source properties", () => {
+ const proto = { inherited: 99 };
+ const source: any = Object.create(proto);
+ source.own = 1;
+ const target: any = {};
+ objDefaults(target, source);
+ assert.equal(target.own, 1);
+ assert.isFalse("inherited" in target);
+ });
+
+ it("should ignore unsafe source keys", () => {
+ const target: any = {};
+ const source: any = {
+ constructor: "bad",
+ prototype: "bad",
+ safe: 1
+ };
+ Object.defineProperty(source, "__proto__", {
+ configurable: true,
+ enumerable: true,
+ value: { polluted: true },
+ writable: true
+ });
+ objDefaults(target, source);
+ assert.equal(target.safe, 1);
+ assert.isFalse(Object.prototype.hasOwnProperty.call(target, "__proto__"));
+ assert.isFalse(Object.prototype.hasOwnProperty.call(target, "constructor"));
+ assert.isFalse(Object.prototype.hasOwnProperty.call(target, "prototype"));
+ assert.isUndefined((target as any).polluted);
+ assert.isUndefined(({} as any).polluted);
+ });
+
+ it("should not write through unsafe targets", () => {
+ const original = (Object.prototype as any).polluted;
+ try {
+ objDefaults(Object.prototype as any, { polluted: "bad" });
+ assert.equal((Object.prototype as any).polluted, original);
+ } finally {
+ if (original === undefined) {
+ delete (Object.prototype as any).polluted;
+ } else {
+ (Object.prototype as any).polluted = original;
+ }
+ }
+ });
+
+ it("should handle target with no existing properties", () => {
+ const target: any = {};
+ objDefaults(target, { x: 10, y: 20 });
+ assert.deepEqual(target, { x: 10, y: 20 });
+ });
+
+ it("should apply enumerable symbol keyed defaults", () => {
+ if (!hasSymbol()) {
+ return;
+ }
+
+ const sym = Symbol("default");
+ const target: any = {};
+ const source: any = {};
+ source[sym] = 99;
+
+ objDefaults(target, source);
+
+ assert.equal(target[sym], 99);
+ });
+
+ it("should not apply non-enumerable symbol keyed defaults", () => {
+ if (!hasSymbol()) {
+ return;
+ }
+
+ const sym = Symbol("hiddenDefault");
+ const target: any = {};
+ const source: any = {};
+ Object.defineProperty(source, sym, {
+ value: 7,
+ enumerable: false,
+ configurable: true,
+ writable: true
+ });
+
+ objDefaults(target, source);
+
+ assert.isUndefined(target[sym]);
+ });
+
+ it("should NOT overwrite inherited target values that are already defined (non-undefined)", () => {
+ const proto = { inherited: "fromProto" };
+ const target: any = Object.create(proto);
+ // target does not own "inherited", but resolves it as "fromProto" via prototype
+ objDefaults(target, { inherited: "fromSource" });
+ // inherited is already defined (non-undefined) on the prototype chain → must not change
+ assert.equal(target.inherited, "fromProto", "inherited defined value must not be overwritten");
+ assert.isFalse(Object.prototype.hasOwnProperty.call(target, "inherited"), "own property must not be created");
+ });
+
+ it("should fill an inherited undefined value by assigning an own property", () => {
+ const proto: any = { inherited: undefined };
+ const target: any = Object.create(proto);
+ // target does not own "inherited" and the resolved value is undefined → should fill
+ objDefaults(target, { inherited: "filled" });
+ assert.equal(target.inherited, "filled", "undefined inherited value should be filled");
+ });
+ });
+});
diff --git a/lib/test/src/common/object/diff.test.ts b/lib/test/src/common/object/diff.test.ts
new file mode 100644
index 00000000..92dcd6c2
--- /dev/null
+++ b/lib/test/src/common/object/diff.test.ts
@@ -0,0 +1,146 @@
+/*
+ * @nevware21/ts-utils
+ * https://github.com/nevware21/ts-utils
+ *
+ * Copyright (c) 2026 NevWare21 Solutions LLC
+ * Licensed under the MIT license.
+ */
+
+import { assert } from "@nevware21/tripwire-chai";
+import { objDiff } from "../../../../src/object/diff";
+import { hasSymbol } from "../../../../src/symbol/symbol";
+
+describe("object diff utilities", () => {
+
+ describe("objDiff", () => {
+ it("should return changed values", () => {
+ const prev = { x: 1, y: 2, z: 3 };
+ const next = { x: 1, y: 99, z: 3 };
+ assert.deepEqual(objDiff(prev, next), { y: 99 });
+ });
+
+ it("should return empty object when objects are identical", () => {
+ const obj = { a: 1, b: 2 };
+ assert.deepEqual(objDiff(obj, { a: 1, b: 2 }), {});
+ });
+
+ it("should include keys added in modified", () => {
+ assert.deepEqual(objDiff({ a: 1 }, { a: 1, b: 2 }), { b: 2 });
+ });
+
+ it("should not include keys removed in modified", () => {
+ const result = objDiff({ a: 1, b: 2 }, { a: 1 } as any);
+ assert.isFalse("b" in result);
+ });
+
+ it("should detect null vs undefined as different", () => {
+ assert.deepEqual(objDiff({ a: null } as any, { a: undefined }), { a: undefined });
+ assert.deepEqual(objDiff({ a: undefined } as any, { a: null }), { a: null });
+ });
+
+ it("should detect value-to-null change", () => {
+ assert.deepEqual(objDiff({ a: 1 } as any, { a: null }), { a: null });
+ });
+
+ it("should detect value-to-undefined change", () => {
+ const result = objDiff({ a: 1 }, { a: undefined });
+ assert.isTrue("a" in result);
+ assert.isUndefined((result as any).a);
+ });
+
+ it("should return all keys when all differ", () => {
+ const prev = { a: 1, b: 2 };
+ const next = { a: 10, b: 20 };
+ assert.deepEqual(objDiff(prev, next), { a: 10, b: 20 });
+ });
+
+ it("should handle empty base object", () => {
+ assert.deepEqual(objDiff({}, { a: 1, b: 2 }), { a: 1, b: 2 });
+ });
+
+ it("should handle empty modified object", () => {
+ assert.deepEqual(objDiff({ a: 1 }, {}), {});
+ });
+
+ it("should handle both objects empty", () => {
+ assert.deepEqual(objDiff({}, {}), {});
+ });
+
+ it("should return empty object when base is null", () => {
+ assert.deepEqual(objDiff(null as any, null as any), {});
+ });
+
+ it("should return empty object when modified is null", () => {
+ assert.deepEqual(objDiff({ a: 1 }, null as any), {});
+ });
+
+ it("should use strict equality (no type coercion)", () => {
+ const result = objDiff({ a: 1 } as any, { a: "1" });
+ assert.deepEqual(result, { a: "1" });
+ });
+
+ it("should treat false and 0 as different from undefined", () => {
+ const result = objDiff({} as any, { a: false, b: 0 });
+ assert.deepEqual(result, { a: false, b: 0 });
+ });
+
+ it("should not include inherited properties from modified", () => {
+ const proto = { inherited: 99 };
+ const modified: any = Object.create(proto);
+ modified.own = 1;
+ const result = objDiff({}, modified);
+ assert.equal(result.own, 1);
+ assert.isFalse("inherited" in result);
+ });
+
+ it("should detect object reference changes", () => {
+ const shared = { nested: true };
+ const prev = { ref: shared };
+ const next = { ref: { nested: true } }; // different reference, same shape
+ const result = objDiff(prev, next);
+ // Different reference => included in diff
+ assert.deepEqual(result, { ref: { nested: true } });
+ });
+
+ it("should NOT detect object reference changes when same reference", () => {
+ const shared = { nested: true };
+ const result = objDiff({ ref: shared }, { ref: shared });
+ assert.deepEqual(result, {});
+ });
+
+ it("should detect changes in enumerable symbol properties", () => {
+ if (!hasSymbol()) {
+ return;
+ }
+
+ const sym1 = Symbol("sym1");
+ const sym2 = Symbol("sym2");
+ const prev: any = { a: 1 };
+ prev[sym1] = "old";
+ prev[sym2] = "same";
+
+ const next: any = { a: 1 };
+ next[sym1] = "new";
+ next[sym2] = "same";
+
+ const result = objDiff(prev, next);
+ assert.isFalse("a" in result);
+ assert.equal((result as any)[sym1], "new");
+ assert.isFalse(sym2 in result);
+ });
+
+ it("should include added symbol properties in modified", () => {
+ if (!hasSymbol()) {
+ return;
+ }
+
+ const sym = Symbol("sym");
+ const prev: any = { a: 1 };
+ const next: any = { a: 1 };
+ next[sym] = "added";
+
+ const result = objDiff(prev, next);
+ assert.equal((result as any)[sym], "added");
+ });
+ });
+});
diff --git a/lib/test/src/common/object/forEachOwnKeySafe.test.ts b/lib/test/src/common/object/forEachOwnKeySafe.test.ts
index e2dbe91f..0cc92cf7 100644
--- a/lib/test/src/common/object/forEachOwnKeySafe.test.ts
+++ b/lib/test/src/common/object/forEachOwnKeySafe.test.ts
@@ -7,13 +7,52 @@
*/
import { assert } from "@nevware21/tripwire-chai";
-import { forEachOwnKeySafe } from "../../../../src/object/forEachOwnKeySafe";
+import { forEachOwnKey, forEachOwnKeySafe } from "../../../../src/object/forEachOwnKey";
+import { hasSymbol } from "../../../../src/symbol/symbol";
describe("object forEachOwnKeySafe tests", () => {
+ describe("forEachOwnKey", () => {
+ it("should iterate own enumerable symbol keys", () => {
+ if (!hasSymbol()) {
+ return;
+ }
+
+ const sym = Symbol("key");
+ const source: any = { a: 1 };
+ source[sym] = 2;
+
+ const visited: PropertyKey[] = [];
+ forEachOwnKey(source, (key) => {
+ visited.push(key);
+ });
+
+ assert.include(visited, "a");
+ assert.include(visited, sym);
+ });
+
+ it("should not filter unsafe string keys", () => {
+ const source: any = Object.create(null);
+ source["safe"] = 1;
+ source["constructor"] = 2;
+ source["prototype"] = 3;
+
+ const visited: string[] = [];
+ forEachOwnKey(source, (key) => {
+ if (typeof key === "string") {
+ visited.push(key);
+ }
+ });
+
+ assert.include(visited, "safe");
+ assert.include(visited, "constructor");
+ assert.include(visited, "prototype");
+ });
+ });
+
describe("forEachOwnKeySafe", () => {
it("should iterate own enumerable keys for plain objects", () => {
const obj = { a: 1, b: 2, c: 3 };
- const visited: string[] = [];
+ const visited: PropertyKey[] = [];
forEachOwnKeySafe(obj, (key) => {
visited.push(key);
@@ -29,7 +68,7 @@ describe("object forEachOwnKeySafe tests", () => {
source["constructor"] = 3;
source["prototype"] = 4;
- const visited: string[] = [];
+ const visited: PropertyKey[] = [];
forEachOwnKeySafe(source, (key) => {
visited.push(key);
@@ -43,7 +82,7 @@ describe("object forEachOwnKeySafe tests", () => {
(value as any)["extra"] = 30;
(value as any)["constructor"] = 40;
- const visited: string[] = [];
+ const visited: PropertyKey[] = [];
const copied: any = {};
forEachOwnKeySafe(value, (key, itemValue) => {
@@ -60,7 +99,7 @@ describe("object forEachOwnKeySafe tests", () => {
it("should support break behavior when callback returns -1", () => {
const obj = { a: 1, b: 2, c: 3 };
- const visited: string[] = [];
+ const visited: PropertyKey[] = [];
forEachOwnKeySafe(obj, (key) => {
visited.push(key);
diff --git a/lib/test/src/common/object/for_each_key.test.ts b/lib/test/src/common/object/for_each_key.test.ts
index f4c90bcf..ac7afa0c 100644
--- a/lib/test/src/common/object/for_each_key.test.ts
+++ b/lib/test/src/common/object/for_each_key.test.ts
@@ -7,7 +7,7 @@
*/
import { assert } from "@nevware21/tripwire-chai";
-import { objForEachKey } from "../../../../src/object/for_each_key";
+import { objForEachKey, objForEachKeySafe } from "../../../../src/object/for_each_key";
describe("object for_each_key tests", () => {
describe("objForEachKey", () => {
@@ -195,4 +195,193 @@ describe("object for_each_key tests", () => {
assert.equal(callCount, 0, "Should not call callback for empty objects");
});
});
+
+ describe("objForEachKeySafe", () => {
+ it("should iterate over all own enumerable keys excluding unsafe keys", () => {
+ const obj = { a: 1, b: 2, c: 3 };
+ const visitedKeys: string[] = [];
+ const values: number[] = [];
+
+ objForEachKeySafe(obj, (key, value) => {
+ visitedKeys.push(key);
+ values.push(value as number);
+ });
+
+ assert.deepEqual(visitedKeys.sort(), ["a", "b", "c"], "Should visit all safe keys");
+ assert.deepEqual(values.sort(), [1, 2, 3], "Should pass the correct values");
+ });
+
+ it("should filter out __proto__ key", () => {
+ const obj = { "name": "Alice" } as { [key: string]: string };
+ Object.defineProperty(obj, "__proto__", {
+ configurable: true,
+ enumerable: true,
+ value: "attack",
+ writable: true
+ });
+ const visitedKeys: string[] = [];
+
+ objForEachKeySafe(obj, (key) => {
+ visitedKeys.push(key);
+ });
+
+ assert.isFalse(visitedKeys.includes("__proto__"), "Should not include __proto__");
+ assert.isTrue(visitedKeys.includes("name"), "Should include safe keys");
+ });
+
+ it("should filter out constructor key", () => {
+ const obj = Object.assign({}, { "constructor": "attack", "name": "Bob" });
+ const visitedKeys: string[] = [];
+
+ objForEachKeySafe(obj, (key) => {
+ visitedKeys.push(key);
+ });
+
+ assert.isFalse(visitedKeys.includes("constructor"), "Should not include constructor");
+ assert.isTrue(visitedKeys.includes("name"), "Should include safe keys");
+ });
+
+ it("should filter out prototype key", () => {
+ const obj = Object.assign({}, { "prototype": "attack", "name": "Charlie" });
+ const visitedKeys: string[] = [];
+
+ objForEachKeySafe(obj, (key) => {
+ visitedKeys.push(key);
+ });
+
+ assert.isFalse(visitedKeys.includes("prototype"), "Should not include prototype");
+ assert.isTrue(visitedKeys.includes("name"), "Should include safe keys");
+ });
+
+ it("should filter out multiple unsafe keys", () => {
+ const obj = {
+ "constructor": "attack",
+ "prototype": "attack",
+ "name": "Dave"
+ } as { [key: string]: string };
+
+ Object.defineProperty(obj, "__proto__", {
+ configurable: true,
+ enumerable: true,
+ value: "attack",
+ writable: true
+ });
+
+ const visitedKeys: string[] = [];
+
+ objForEachKeySafe(obj, (key) => {
+ visitedKeys.push(key);
+ });
+
+ assert.deepEqual(visitedKeys.sort(), ["name"], "Should only include safe keys");
+ });
+
+ it("should use the provided thisArg", () => {
+ const obj = { a: 1, b: 2 };
+ const thisObj = { test: "context" };
+ let actualThis: any;
+
+ objForEachKeySafe(obj, function(this: any) {
+ actualThis = this;
+ }, thisObj);
+
+ assert.strictEqual(actualThis, thisObj, "Should use the provided thisArg");
+ });
+
+ it("should use the object as thisArg when not provided", () => {
+ const obj = { a: 1, b: 2 };
+ let actualThis: any;
+
+ objForEachKeySafe(obj, function(this: any) {
+ actualThis = this;
+ });
+
+ assert.strictEqual(actualThis, obj, "Should use the object as thisArg when not provided");
+ });
+
+ it("should break iteration when callback returns -1", () => {
+ const obj = { a: 1, b: 2, c: 3, d: 4 };
+ const visitedKeys: string[] = [];
+
+ objForEachKeySafe(obj, (key) => {
+ visitedKeys.push(key);
+ if (key === "b") {
+ return -1;
+ }
+ });
+
+ assert.isTrue(
+ visitedKeys.length <= 2,
+ "Should stop iteration when callback returns -1"
+ );
+ assert.isFalse(
+ visitedKeys.includes("c") || visitedKeys.includes("d"),
+ "Should not visit keys after returning -1"
+ );
+ });
+
+ it("should handle null or undefined objects", () => {
+ let callCount = 0;
+
+ objForEachKeySafe(null, () => {
+ callCount++;
+ });
+
+ objForEachKeySafe(undefined, () => {
+ callCount++;
+ });
+
+ assert.equal(callCount, 0, "Should not call callback for null or undefined objects");
+ });
+
+ it("should handle non-object values", () => {
+ let callCount = 0;
+
+ objForEachKeySafe(42 as any, () => {
+ callCount++;
+ });
+
+ objForEachKeySafe("string" as any, () => {
+ callCount++;
+ });
+
+ assert.equal(callCount, 0, "Should not call callback for non-object values");
+ });
+
+ it("should work with complex objects and filter unsafe keys", () => {
+ const complexObj = Object.assign({}, {
+ num: 42,
+ str: "hello",
+ bool: true,
+ arr: [1, 2, 3],
+ nested: { a: 1, b: 2 },
+ func: function() {
+ return "test";
+ },
+ "__proto__": "attack"
+ });
+
+ const keys: string[] = [];
+
+ objForEachKeySafe(complexObj, (key) => {
+ keys.push(key);
+ });
+
+ assert.isFalse(keys.includes("__proto__"), "Should not include __proto__");
+ assert.equal(keys.length, 6, "Should iterate over all safe keys in a complex object");
+ assert.includeMembers(keys, ["num", "str", "bool", "arr", "nested", "func"],
+ "Should include all safe keys");
+ });
+
+ it("should work with empty objects", () => {
+ const emptyObj = {};
+ let callCount = 0;
+
+ objForEachKeySafe(emptyObj, () => {
+ callCount++;
+ });
+
+ assert.equal(callCount, 0, "Should not call callback for empty objects");
+ });
+ });
});
diff --git a/lib/test/src/common/object/isUnsafeTarget.test.ts b/lib/test/src/common/object/isUnsafeTarget.test.ts
index bacffafd..ee638421 100644
--- a/lib/test/src/common/object/isUnsafeTarget.test.ts
+++ b/lib/test/src/common/object/isUnsafeTarget.test.ts
@@ -8,6 +8,7 @@
import { assert } from "@nevware21/tripwire-chai";
import { isUnsafeTarget } from "../../../../src/object/isUnsafeTarget";
+import { getInst } from "../../../../src/helpers/environment";
describe("object isUnsafeTarget tests", () => {
it("should identify common built-in prototype objects", () => {
@@ -46,4 +47,105 @@ describe("object isUnsafeTarget tests", () => {
assert.isFalse(isUnsafeTarget("test"), "string should be safe");
assert.isFalse(isUnsafeTarget(false), "boolean should be safe");
});
+
+ it("should identify ES6 collection prototype objects when available", () => {
+ if (typeof Map !== "undefined") {
+ assert.isTrue(isUnsafeTarget(Map.prototype), "Map.prototype should be unsafe");
+ }
+ if (typeof Set !== "undefined") {
+ assert.isTrue(isUnsafeTarget(Set.prototype), "Set.prototype should be unsafe");
+ }
+ if (typeof WeakMap !== "undefined") {
+ assert.isTrue(isUnsafeTarget(WeakMap.prototype), "WeakMap.prototype should be unsafe");
+ }
+ if (typeof WeakSet !== "undefined") {
+ assert.isTrue(isUnsafeTarget(WeakSet.prototype), "WeakSet.prototype should be unsafe");
+ }
+ if (typeof Promise !== "undefined") {
+ assert.isTrue(isUnsafeTarget(Promise.prototype), "Promise.prototype should be unsafe");
+ }
+ if (typeof Symbol !== "undefined") {
+ assert.isTrue(isUnsafeTarget((Symbol as any).prototype), "Symbol.prototype should be unsafe");
+ }
+ });
+
+ it("should identify ArrayBuffer and DataView prototype objects when available", () => {
+ if (typeof ArrayBuffer !== "undefined") {
+ assert.isTrue(isUnsafeTarget(ArrayBuffer.prototype), "ArrayBuffer.prototype should be unsafe");
+ }
+ let sharedArrayBuffer: any = getInst("SharedArrayBuffer");
+ if (sharedArrayBuffer) {
+ assert.isTrue(isUnsafeTarget(sharedArrayBuffer["prototype"]), "SharedArrayBuffer.prototype should be unsafe");
+ }
+ if (typeof DataView !== "undefined") {
+ assert.isTrue(isUnsafeTarget(DataView.prototype), "DataView.prototype should be unsafe");
+ }
+ });
+
+ it("should identify typed-array prototype objects when available", () => {
+ if (typeof Int8Array !== "undefined") {
+ assert.isTrue(isUnsafeTarget(Int8Array.prototype), "Int8Array.prototype should be unsafe");
+ }
+ if (typeof Uint8Array !== "undefined") {
+ assert.isTrue(isUnsafeTarget(Uint8Array.prototype), "Uint8Array.prototype should be unsafe");
+ }
+ if (typeof Uint8ClampedArray !== "undefined") {
+ assert.isTrue(isUnsafeTarget(Uint8ClampedArray.prototype), "Uint8ClampedArray.prototype should be unsafe");
+ }
+ if (typeof Int16Array !== "undefined") {
+ assert.isTrue(isUnsafeTarget(Int16Array.prototype), "Int16Array.prototype should be unsafe");
+ }
+ if (typeof Uint16Array !== "undefined") {
+ assert.isTrue(isUnsafeTarget(Uint16Array.prototype), "Uint16Array.prototype should be unsafe");
+ }
+ if (typeof Int32Array !== "undefined") {
+ assert.isTrue(isUnsafeTarget(Int32Array.prototype), "Int32Array.prototype should be unsafe");
+ }
+ if (typeof Uint32Array !== "undefined") {
+ assert.isTrue(isUnsafeTarget(Uint32Array.prototype), "Uint32Array.prototype should be unsafe");
+ }
+ if (typeof Float32Array !== "undefined") {
+ assert.isTrue(isUnsafeTarget(Float32Array.prototype), "Float32Array.prototype should be unsafe");
+ }
+ if (typeof Float64Array !== "undefined") {
+ assert.isTrue(isUnsafeTarget(Float64Array.prototype), "Float64Array.prototype should be unsafe");
+ }
+ let bigInt64Array: any = getInst("BigInt64Array");
+ if (bigInt64Array) {
+ assert.isTrue(isUnsafeTarget(bigInt64Array["prototype"]), "BigInt64Array.prototype should be unsafe");
+ }
+ let bigUint64Array: any = getInst("BigUint64Array");
+ if (bigUint64Array) {
+ assert.isTrue(isUnsafeTarget(bigUint64Array["prototype"]), "BigUint64Array.prototype should be unsafe");
+ }
+ });
+
+ it("should identify WeakRef and FinalizationRegistry prototype objects when available", () => {
+ let weakRef: any = getInst("WeakRef");
+ if (weakRef) {
+ assert.isTrue(isUnsafeTarget(weakRef["prototype"]), "WeakRef.prototype should be unsafe");
+ }
+ let finalizationRegistry: any = getInst("FinalizationRegistry");
+ if (finalizationRegistry) {
+ assert.isTrue(isUnsafeTarget(finalizationRegistry["prototype"]), "FinalizationRegistry.prototype should be unsafe");
+ }
+ });
+
+ it("should return false for instances of the new built-in types", () => {
+ if (typeof ArrayBuffer !== "undefined") {
+ assert.isFalse(isUnsafeTarget(new ArrayBuffer(8)), "ArrayBuffer instance should be safe");
+ }
+ if (typeof DataView !== "undefined") {
+ assert.isFalse(isUnsafeTarget(new DataView(new ArrayBuffer(8))), "DataView instance should be safe");
+ }
+ if (typeof Uint8Array !== "undefined") {
+ assert.isFalse(isUnsafeTarget(new Uint8Array(4)), "Uint8Array instance should be safe");
+ }
+ if (typeof Map !== "undefined") {
+ assert.isFalse(isUnsafeTarget(new Map()), "Map instance should be safe");
+ }
+ if (typeof Set !== "undefined") {
+ assert.isFalse(isUnsafeTarget(new Set()), "Set instance should be safe");
+ }
+ });
});
diff --git a/lib/test/src/common/object/map_values.test.ts b/lib/test/src/common/object/map_values.test.ts
new file mode 100644
index 00000000..160124c1
--- /dev/null
+++ b/lib/test/src/common/object/map_values.test.ts
@@ -0,0 +1,109 @@
+/*
+ * @nevware21/ts-utils
+ * https://github.com/nevware21/ts-utils
+ *
+ * Copyright (c) 2026 NevWare21 Solutions LLC
+ * Licensed under the MIT license.
+ */
+
+import { assert } from "@nevware21/tripwire-chai";
+import { objMapValues } from "../../../../src/object/map_values";
+import { hasSymbol } from "../../../../src/symbol/symbol";
+
+describe("object map_values utilities", () => {
+
+ describe("objMapValues", () => {
+ it("should map all values with a transform function", () => {
+ const obj = { a: 1, b: 2, c: 3 };
+ assert.deepEqual(objMapValues(obj, (v) => v * 2), { a: 2, b: 4, c: 6 });
+ });
+
+ it("should pass the key as the second argument to mapper", () => {
+ const obj = { x: 10, y: 20 };
+ const keysReceived: PropertyKey[] = [];
+ objMapValues(obj, (v, k) => {
+ keysReceived.push(k);
+ return v;
+ });
+ assert.deepEqual(keysReceived.sort(), ["x", "y"]);
+ });
+
+ it("should support changing value type", () => {
+ const obj = { firstName: "ada", lastName: "lovelace" };
+ const result = objMapValues(obj, (v) => v.toUpperCase());
+ assert.deepEqual(result, { firstName: "ADA", lastName: "LOVELACE" });
+ });
+
+ it("should return empty object when source is null", () => {
+ assert.deepEqual(objMapValues(null as any, (v) => v), {});
+ });
+
+ it("should return empty object when source is undefined", () => {
+ assert.deepEqual(objMapValues(undefined as any, (v) => v), {});
+ });
+
+ it("should return empty object when source has no own properties", () => {
+ assert.deepEqual(objMapValues({}, (v) => v), {});
+ });
+
+ it("should handle mapper returning undefined", () => {
+ const obj = { a: 1, b: 2 };
+ const result = objMapValues(obj, () => undefined);
+ assert.isTrue("a" in result);
+ assert.isTrue("b" in result);
+ assert.isUndefined(result.a);
+ assert.isUndefined(result.b);
+ });
+
+ it("should handle mapper returning null", () => {
+ const obj = { a: 1, b: 2 };
+ const result = objMapValues(obj, () => null);
+ assert.isNull(result.a);
+ assert.isNull(result.b);
+ });
+
+ it("should not include inherited properties", () => {
+ const proto = { inherited: 42 };
+ const child: any = Object.create(proto);
+ child.own = 1;
+ const result = objMapValues(child, (v) => v * 10);
+ assert.deepEqual(result, { own: 10 });
+ });
+
+ it("should produce a new independent object", () => {
+ const obj = { a: 1 };
+ const result = objMapValues(obj, (v) => v);
+ result.a = 99;
+ assert.equal(obj.a, 1, "original should not be mutated");
+ });
+
+ it("should handle objects with many properties", () => {
+ const obj: Record = {};
+ for (let i = 0; i < 100; i++) {
+ obj["k" + i] = i;
+ }
+ const result = objMapValues(obj, (v) => v + 1);
+ for (let i = 0; i < 100; i++) {
+ assert.equal(result["k" + i], i + 1);
+ }
+ });
+
+ it("should map enumerable symbol properties", () => {
+ if (!hasSymbol()) {
+ return;
+ }
+
+ const sym1 = Symbol("sym1");
+ const sym2 = Symbol("sym2");
+ const obj: any = { a: 1, b: 2 };
+ obj[sym1] = 10;
+ obj[sym2] = 20;
+
+ const result = objMapValues(obj, (v) => (v as number) * 2);
+ assert.equal(result.a, 2);
+ assert.equal(result.b, 4);
+ assert.equal((result as any)[sym1], 20);
+ assert.equal((result as any)[sym2], 40);
+ });
+ });
+});
diff --git a/lib/test/src/common/object/pick.test.ts b/lib/test/src/common/object/pick.test.ts
new file mode 100644
index 00000000..6c3fdf24
--- /dev/null
+++ b/lib/test/src/common/object/pick.test.ts
@@ -0,0 +1,246 @@
+/*
+ * @nevware21/ts-utils
+ * https://github.com/nevware21/ts-utils
+ *
+ * Copyright (c) 2026 NevWare21 Solutions LLC
+ * Licensed under the MIT license.
+ */
+
+import { assert } from "@nevware21/tripwire-chai";
+import { objPick, objOmit, objPickBy, objOmitBy } from "../../../../src/object/pick";
+import { hasSymbol } from "../../../../src/symbol/symbol";
+
+describe("object pick utilities", () => {
+
+ // ─── objPick ────────────────────────────────────────────────────────────────
+
+ describe("objPick", () => {
+ it("should pick specified keys", () => {
+ const obj = { a: 1, b: "hello", c: true };
+ assert.deepEqual(objPick(obj, ["a", "c"] as const), { a: 1, c: true });
+ });
+
+ it("should pick a single key", () => {
+ const obj = { x: 10, y: 20 };
+ assert.deepEqual(objPick(obj, ["x"] as const), { x: 10 });
+ });
+
+ it("should return empty object for empty keys array", () => {
+ const obj = { a: 1, b: 2 };
+ assert.deepEqual(objPick(obj, []), {});
+ });
+
+ it("should silently skip keys not present on source", () => {
+ const obj = { a: 1 };
+ const result = objPick(obj, ["a", "missing" as any]);
+ assert.deepEqual(result, { a: 1 });
+ assert.isFalse("missing" in result);
+ });
+
+ it("should return empty object when source is null", () => {
+ assert.deepEqual(objPick(null as any, ["a"]) as any, {});
+ });
+
+ it("should return empty object when source is undefined", () => {
+ assert.deepEqual(objPick(undefined as any, ["a"]) as any, {});
+ });
+
+ it("should handle null values in the object", () => {
+ const obj: { a: null; b: number } = { a: null, b: 2 };
+ assert.deepEqual(objPick(obj, ["a"]), { a: null } as Pick);
+ });
+
+ it("should handle undefined values in the object", () => {
+ const obj: { a: undefined; b: number } = { a: undefined, b: 2 };
+ const result = objPick(obj, ["a"]);
+ assert.isTrue("a" in result);
+ assert.isUndefined(result.a);
+ });
+
+ it("should not include inherited properties", () => {
+ const proto = { inherited: 42 };
+ const obj = Object.create(proto);
+ obj.own = 1;
+ const result = objPick(obj, ["own", "inherited"]);
+ assert.isTrue("own" in result);
+ assert.isFalse("inherited" in result);
+ });
+
+ it("should preserve all picked values", () => {
+ const obj = { a: 1, b: 2, c: 3, d: 4 };
+ const result = objPick(obj, ["a", "b", "c", "d"]);
+ assert.deepEqual(result, obj);
+ });
+
+ it("should not pick non-enumerable own properties", () => {
+ const obj: any = {};
+ Object.defineProperty(obj, "hidden", {
+ value: 1,
+ enumerable: false
+ });
+ obj.visible = 2;
+
+ const result = objPick(obj, ["hidden", "visible"]);
+ assert.equal((result as any).visible, 2);
+ assert.isFalse("hidden" in result);
+ });
+ });
+
+ // ─── objOmit ────────────────────────────────────────────────────────────────
+
+ describe("objOmit", () => {
+ it("should omit specified keys", () => {
+ const obj = { a: 1, b: "hello", c: true };
+ assert.deepEqual(objOmit(obj, ["b"]), { a: 1, c: true });
+ });
+
+ it("should omit multiple keys", () => {
+ const obj = { a: 1, b: 2, c: 3 };
+ assert.deepEqual(objOmit(obj, ["a", "c"]), { b: 2 });
+ });
+
+ it("should return a full copy when keys array is empty", () => {
+ const obj = { a: 1, b: 2 };
+ assert.deepEqual(objOmit(obj, []), { a: 1, b: 2 });
+ });
+
+ it("should silently ignore keys not on source", () => {
+ const obj = { a: 1, b: 2 };
+ assert.deepEqual(objOmit(obj, ["missing" as any]), { a: 1, b: 2 });
+ });
+
+ it("should return empty object when source is null", () => {
+ assert.deepEqual(objOmit(null as any, ["a"]), {});
+ });
+
+ it("should return empty object when source is undefined", () => {
+ assert.deepEqual(objOmit(undefined as any, ["a"]), {});
+ });
+
+ it("should not include inherited properties", () => {
+ const proto = { inherited: 42 };
+ const child: any = Object.create(proto);
+ child.own = 1;
+ child.extra = 2;
+ const result = objOmit(child, ["extra"]);
+ assert.deepEqual(result, { own: 1 });
+ });
+
+ it("should omit all keys when all are specified", () => {
+ const obj = { a: 1, b: 2 };
+ assert.deepEqual(objOmit(obj, ["a", "b"]), {});
+ });
+
+ it("should omit numeric keys using their runtime property names", () => {
+ const obj = { 1: "x", 2: "y" };
+ assert.deepEqual(objOmit(obj, [1]), { 2: "y" });
+ });
+
+ it("should preserve enumerable symbol properties not in keys", () => {
+ if (!hasSymbol()) {
+ return;
+ }
+
+ const sym1 = Symbol("sym1");
+ const sym2 = Symbol("sym2");
+ const obj: any = { a: 1, b: 2 };
+ obj[sym1] = "symbol1";
+ obj[sym2] = "symbol2";
+
+ const result = objOmit(obj, ["a", sym1]);
+ assert.equal(result.b, 2);
+ assert.isFalse("a" in result);
+ assert.equal(result[sym2], "symbol2");
+ assert.isFalse(sym1 in result);
+ });
+ });
+
+ // ─── objPickBy ──────────────────────────────────────────────────────────────
+
+ describe("objPickBy", () => {
+ it("should pick by value predicate", () => {
+ const obj = { a: 1, b: 2, c: 3, d: 4 };
+ assert.deepEqual(objPickBy(obj, (_k, v) => v > 2), { c: 3, d: 4 });
+ });
+
+ it("should pick by key predicate", () => {
+ const obj = { a: 1, b: 2, c: 3 };
+ assert.deepEqual(objPickBy(obj, (k) => k !== "b"), { a: 1, c: 3 });
+ });
+
+ it("should return empty object when no property matches", () => {
+ const obj = { a: 1, b: 2 };
+ assert.deepEqual(objPickBy(obj, () => false), {});
+ });
+
+ it("should return all properties when all match", () => {
+ const obj = { a: 1, b: 2 };
+ assert.deepEqual(objPickBy(obj, () => true), { a: 1, b: 2 });
+ });
+
+ it("should return empty object when source is null or undefined", () => {
+ assert.deepEqual(objPickBy(null as any, () => true), {});
+ assert.deepEqual(objPickBy(undefined as any, () => true), {});
+ });
+
+ it("should receive correct key and value in predicate", () => {
+ const obj = { x: 10 };
+ let capturedKey: PropertyKey | undefined;
+ let capturedVal: any;
+ objPickBy(obj, (k, v) => {
+ capturedKey = k;
+ capturedVal = v;
+ return true;
+ });
+ assert.equal(capturedKey, "x");
+ assert.equal(capturedVal, 10);
+ });
+
+ it("should not include inherited properties", () => {
+ const proto = { inherited: 99 };
+ const child: any = Object.create(proto);
+ child.own = 1;
+ const result = objPickBy(child, () => true);
+ assert.deepEqual(result, { own: 1 });
+ });
+ });
+
+ // ─── objOmitBy ──────────────────────────────────────────────────────────────
+
+ describe("objOmitBy", () => {
+ it("should omit by value predicate", () => {
+ const obj = { a: 1, b: 2, c: 3, d: 4 };
+ assert.deepEqual(objOmitBy(obj, (_k, v) => v > 2), { a: 1, b: 2 });
+ });
+
+ it("should omit by key predicate", () => {
+ const obj = { a: 1, b: 2, c: 3 };
+ assert.deepEqual(objOmitBy(obj, (k) => k === "b"), { a: 1, c: 3 });
+ });
+
+ it("should return full copy when no property matches predicate", () => {
+ const obj = { a: 1, b: 2 };
+ assert.deepEqual(objOmitBy(obj, () => false), { a: 1, b: 2 });
+ });
+
+ it("should return empty object when all properties match predicate", () => {
+ const obj = { a: 1, b: 2 };
+ assert.deepEqual(objOmitBy(obj, () => true), {});
+ });
+
+ it("should return empty object when source is null or undefined", () => {
+ assert.deepEqual(objOmitBy(null as any, () => true), {});
+ assert.deepEqual(objOmitBy(undefined as any, () => true), {});
+ });
+
+ it("should be the inverse of objPickBy", () => {
+ const obj = { a: 1, b: 2, c: 3 };
+ const pred = (_k: string, v: number) => v === 2;
+ const picked = objPickBy(obj, pred);
+ const omitted = objOmitBy(obj, pred);
+ // The union of keys in picked + omitted should equal the original keys
+ const allKeys = Object.keys(picked).concat(Object.keys(omitted)).sort();
+ assert.deepEqual(allKeys, ["a", "b", "c"]);
+ });
+ });
+});
diff --git a/lib/test/src/common/object/property_is_enumerable.test.ts b/lib/test/src/common/object/property_is_enumerable.test.ts
index f6c5417e..cd81b9ad 100644
--- a/lib/test/src/common/object/property_is_enumerable.test.ts
+++ b/lib/test/src/common/object/property_is_enumerable.test.ts
@@ -7,7 +7,7 @@
*/
import { assert } from "@nevware21/tripwire-chai";
-import { objPropertyIsEnumerable } from "../../../../src/object/property_is_enumerable";
+import { _objPropertyIsEnumerable, objPropertyIsEnumerable } from "../../../../src/object/property_is_enumerable";
import { ObjClass } from "../../../../src/internal/constants";
describe("object property_is_enumerable tests", () => {
@@ -215,12 +215,98 @@ describe("object property_is_enumerable tests", () => {
assert.isFalse(objPropertyIsEnumerable(obj, "nonEnumInherited"),
"Non-enumerable inherited property should return false with fallback");
- assert.isTrue(objPropertyIsEnumerable(obj, "hello"),
- "Enumerable inherited property should return true with fallback");
+ assert.isFalse(objPropertyIsEnumerable(obj, "hello"),
+ "Enumerable inherited property should return false with fallback");
} finally {
// Restore the original function
ObjClass.getOwnPropertyDescriptor = originalGetOwnPropDesc;
}
});
});
+
+ describe("_objPropertyIsEnumerable", () => {
+ it("should identify own enumerable and non-enumerable properties", () => {
+ const obj: any = {
+ visible: true
+ };
+
+ Object.defineProperty(obj, "hidden", {
+ value: true,
+ enumerable: false
+ });
+
+ assert.isTrue(_objPropertyIsEnumerable(obj, "visible"), "Own enumerable property should return true");
+ assert.isFalse(_objPropertyIsEnumerable(obj, "hidden"), "Own non-enumerable property should return false");
+ assert.isFalse(_objPropertyIsEnumerable(obj, "missing"), "Missing property should return false");
+ });
+
+ it("should return false for inherited enumerable properties", () => {
+ const proto: any = {
+ inherited: true
+ };
+ const obj = Object.create(proto);
+
+ assert.isFalse(_objPropertyIsEnumerable(obj, "inherited"), "Inherited enumerable property should return false");
+ });
+
+ it("should support symbol keys", () => {
+ if (typeof Symbol !== "function") {
+ return;
+ }
+
+ const visibleSym = Symbol("visibleSym");
+ const hiddenSym = Symbol("hiddenSym");
+ const obj: any = {};
+
+ obj[visibleSym] = 1;
+ Object.defineProperty(obj, hiddenSym, {
+ value: 2,
+ enumerable: false
+ });
+
+ assert.isTrue(_objPropertyIsEnumerable(obj, visibleSym), "Own enumerable symbol property should return true");
+ assert.isFalse(_objPropertyIsEnumerable(obj, hiddenSym), "Own non-enumerable symbol property should return false");
+ });
+
+ it("should not use an instance propertyIsEnumerable override function", () => {
+ const obj: any = {
+ test: true
+ };
+ let called = false;
+
+ obj.propertyIsEnumerable = function() {
+ called = true;
+ return false;
+ };
+
+ assert.isTrue(_objPropertyIsEnumerable(obj, "test"), "Should use prototype implementation and return true");
+ assert.isFalse(called, "Should not call the instance propertyIsEnumerable override");
+ });
+
+ it("should not throw when instance propertyIsEnumerable is non-callable", () => {
+ const obj: any = {
+ test: true,
+ propertyIsEnumerable: 1
+ };
+
+ assert.doesNotThrow(() => {
+ assert.isTrue(_objPropertyIsEnumerable(obj, "test"), "Should still return correct enumerability result");
+ }, "Non-callable instance override must not be invoked");
+ });
+
+ it("should propagate errors for null/undefined unlike objPropertyIsEnumerable which uses safe()", () => {
+ // _objPropertyIsEnumerable is built with _unwrapFunctionNoInstWithPoly, which captures
+ // ObjProto.propertyIsEnumerable at module-load time. That cached reference cannot be
+ // replaced at runtime, so a "native unavailable" scenario cannot be simulated in a live
+ // test. The meaningful behavioral difference to verify here is that _objPropertyIsEnumerable
+ // does NOT wrap calls in safe(), so passing null or undefined lets the underlying native
+ // TypeError propagate — in contrast to objPropertyIsEnumerable, which returns false.
+ assert.isFalse(objPropertyIsEnumerable(null as any, "prop"), "objPropertyIsEnumerable should return false for null (safe guard)");
+ assert.isFalse(objPropertyIsEnumerable(undefined as any, "prop"), "objPropertyIsEnumerable should return false for undefined (safe guard)");
+
+ // Both should throw a TypeError ("Cannot convert undefined or null to object")
+ assert.throws(() => _objPropertyIsEnumerable(null as any, "prop"));
+ assert.throws(() => _objPropertyIsEnumerable(undefined as any, "prop"));
+ });
+ });
});
diff --git a/package.json b/package.json
index 30685aff..e2bb528f 100644
--- a/package.json
+++ b/package.json
@@ -237,10 +237,10 @@
"devDependencies": {
"@istanbuljs/nyc-config-typescript": "^1.0.2",
"@microsoft/api-extractor": "^7.57.7",
- "@nevware21/coverage-tools": ">= 0.1.4 < 2.x",
+ "@nevware21/coverage-tools": ">= 0.1.5 < 2.x",
"@nevware21/grunt-eslint-ts": "^0.5.1",
"@nevware21/grunt-ts-plugin": "^0.5.1",
- "@nevware21/publish-npm": ">= 0.1.4 < 2.x",
+ "@nevware21/publish-npm": ">= 0.1.5 < 2.x",
"@nevware21/tripwire-chai": ">= 0.1.8 < 2.x",
"@rollup/plugin-commonjs": "^29.0.0",
"@rollup/plugin-json": "^6.0.0",