diff --git a/.size-limit.json b/.size-limit.json index c13b18d9..81319fe2 100644 --- a/.size-limit.json +++ b/.size-limit.json @@ -2,42 +2,42 @@ { "name": "es5-full", "path": "lib/dist/es5/mod/ts-utils.js", - "limit": "31 kb", + "limit": "32.5 kb", "brotli": false, "running": false }, { "name": "es6-full", "path": "lib/dist/es6/mod/ts-utils.js", - "limit": "30 kb", + "limit": "31.5 kb", "brotli": false, "running": false }, { "name": "es5-full-brotli", "path": "lib/dist/es5/mod/ts-utils.js", - "limit": "11 kb", + "limit": "11.5 kb", "brotli": true, "running": false }, { "name": "es6-full-brotli", "path": "lib/dist/es6/mod/ts-utils.js", - "limit": "11 kb", + "limit": "11.5 kb", "brotli": true, "running": false }, { "name": "es5-zip", "path": "lib/dist/es5/mod/ts-utils.js", - "limit": "12 Kb", + "limit": "12.5 Kb", "gzip": true, "running": false }, { "name": "es6-zip", "path": "lib/dist/es6/mod/ts-utils.js", - "limit": "12 Kb", + "limit": "12.5 Kb", "gzip": true, "running": false }, diff --git a/README.md b/README.md index edf3982f..0a1c608d 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,8 @@ npm install @nevware21/ts-utils --save - Property manipulation - Deep copy/extend - Object transformation helpers +- Property selection and omission helpers +- Shallow defaults, conditional merges, and object diff helpers ### Security Helpers - Safe iteration over own enumerable keys while skipping dangerous property names @@ -127,7 +129,7 @@ Below is a categorized list of all available utilities with direct links to thei | Iterator | [createArrayIterator](https://nevware21.github.io/ts-utils/typedoc/functions/createArrayIterator.html)(); [createIterator](https://nevware21.github.io/ts-utils/typedoc/functions/createIterator.html)(); [createIterable](https://nevware21.github.io/ts-utils/typedoc/functions/createIterable.html)(); [createRangeIterator](https://nevware21.github.io/ts-utils/typedoc/functions/createRangeIterator.html)(); [iterForOf](https://nevware21.github.io/ts-utils/typedoc/functions/iterForOf.html)(); [isAsyncIterable](https://nevware21.github.io/ts-utils/typedoc/functions/isAsyncIterable.html)(); [isIterable](https://nevware21.github.io/ts-utils/typedoc/functions/isIterable.html)(); [isIterator](https://nevware21.github.io/ts-utils/typedoc/functions/isIterator.html)(); [makeIterable](https://nevware21.github.io/ts-utils/typedoc/functions/makeIterable.html)(); [arrAppend](https://nevware21.github.io/ts-utils/typedoc/functions/arrAppend.html)(); [arrFrom](https://nevware21.github.io/ts-utils/typedoc/functions/arrFrom.html)(); | Number | [getIntValue](https://nevware21.github.io/ts-utils/typedoc/functions/getIntValue.html)(); [isInteger](https://nevware21.github.io/ts-utils/typedoc/functions/isInteger.html)(); [isIntegerInRange](https://nevware21.github.io/ts-utils/typedoc/functions/isIntegerInRange.html)(); [isFiniteNumber](https://nevware21.github.io/ts-utils/typedoc/functions/isFiniteNumber.html)(); [isNumber](https://nevware21.github.io/ts-utils/typedoc/functions/isNumber.html)(); | Math | [mathAbs](https://nevware21.github.io/ts-utils/typedoc/functions/mathAbs.html)(); [mathAcos](https://nevware21.github.io/ts-utils/typedoc/functions/mathAcos.html)(); [mathAsin](https://nevware21.github.io/ts-utils/typedoc/functions/mathAsin.html)(); [mathAtan](https://nevware21.github.io/ts-utils/typedoc/functions/mathAtan.html)(); [mathAtan2](https://nevware21.github.io/ts-utils/typedoc/functions/mathAtan2.html)(); [mathCeil](https://nevware21.github.io/ts-utils/typedoc/functions/mathCeil.html)(); [mathCos](https://nevware21.github.io/ts-utils/typedoc/functions/mathCos.html)(); [mathExp](https://nevware21.github.io/ts-utils/typedoc/functions/mathExp.html)(); [mathFloor](https://nevware21.github.io/ts-utils/typedoc/functions/mathFloor.html)(); [mathLog](https://nevware21.github.io/ts-utils/typedoc/functions/mathLog.html)(); [mathMax](https://nevware21.github.io/ts-utils/typedoc/functions/mathMax.html)(); [mathMin](https://nevware21.github.io/ts-utils/typedoc/functions/mathMin.html)(); [mathPow](https://nevware21.github.io/ts-utils/typedoc/functions/mathPow.html)(); [mathRandom](https://nevware21.github.io/ts-utils/typedoc/functions/mathRandom.html)(); [mathRound](https://nevware21.github.io/ts-utils/typedoc/functions/mathRound.html)(); [mathSin](https://nevware21.github.io/ts-utils/typedoc/functions/mathSin.html)(); [mathSqrt](https://nevware21.github.io/ts-utils/typedoc/functions/mathSqrt.html)(); [mathTan](https://nevware21.github.io/ts-utils/typedoc/functions/mathTan.html)(); [mathToInt](https://nevware21.github.io/ts-utils/typedoc/functions/mathToInt.html)(); [mathTrunc](https://nevware21.github.io/ts-utils/typedoc/functions/mathTrunc.html)(); -| Object | [deepExtend](https://nevware21.github.io/ts-utils/typedoc/functions/deepExtend.html)(); [forEachOwnKeySafe](https://nevware21.github.io/ts-utils/typedoc/functions/forEachOwnKeySafe.html)(); [isObject](https://nevware21.github.io/ts-utils/typedoc/functions/isObject.html)(); [isUnsafePropKey](https://nevware21.github.io/ts-utils/typedoc/functions/isUnsafePropKey.html)(); [isUnsafeTarget](https://nevware21.github.io/ts-utils/typedoc/functions/isUnsafeTarget.html)(); [objAssign](https://nevware21.github.io/ts-utils/typedoc/functions/objAssign.html)(); [objCopyProps](https://nevware21.github.io/ts-utils/typedoc/functions/objCopyProps.html)(); [objCreate](https://nevware21.github.io/ts-utils/typedoc/functions/objCreate.html)(); [objDeepCopy](https://nevware21.github.io/ts-utils/typedoc/functions/objDeepCopy.html)(); [objDeepFreeze](https://nevware21.github.io/ts-utils/typedoc/functions/objDeepFreeze.html)(); [objDefine](https://nevware21.github.io/ts-utils/typedoc/functions/objDefine.html)(); [objDefineAccessors](https://nevware21.github.io/ts-utils/typedoc/functions/objDefineAccessors.html)(); [objDefineGet](https://nevware21.github.io/ts-utils/typedoc/functions/objDefineGet.html)(); [objDefineProp](https://nevware21.github.io/ts-utils/typedoc/functions/objDefineProp.html)(); [objDefineProps](https://nevware21.github.io/ts-utils/typedoc/functions/objDefineProps.html)(); [objDefineProperties](https://nevware21.github.io/ts-utils/typedoc/functions/objDefineProperties.html)(); [objEntries](https://nevware21.github.io/ts-utils/typedoc/functions/objEntries.html)(); [objExtend](https://nevware21.github.io/ts-utils/typedoc/functions/objExtend.html)(); [objForEachKey](https://nevware21.github.io/ts-utils/typedoc/functions/objForEachKey.html)(); [objFreeze](https://nevware21.github.io/ts-utils/typedoc/functions/objFreeze.html)(); [objFromEntries](https://nevware21.github.io/ts-utils/typedoc/functions/objFromEntries.html)(); [objGetOwnPropertyDescriptor](https://nevware21.github.io/ts-utils/typedoc/functions/objGetOwnPropertyDescriptor.html)(); [objGetOwnPropertyDescriptors](https://nevware21.github.io/ts-utils/typedoc/functions/objGetOwnPropertyDescriptors.html)(); [objGetOwnPropertyNames](https://nevware21.github.io/ts-utils/typedoc/functions/objGetOwnPropertyNames.html)(); [objGetOwnPropertySymbols](https://nevware21.github.io/ts-utils/typedoc/functions/objGetOwnPropertySymbols.html)(); [objHasOwn](https://nevware21.github.io/ts-utils/typedoc/functions/objHasOwn.html)(); [objHasOwnProperty](https://nevware21.github.io/ts-utils/typedoc/functions/objHasOwnProperty.html)(); [objIs](https://nevware21.github.io/ts-utils/typedoc/functions/objIs.html)(); [objIsExtensible](https://nevware21.github.io/ts-utils/typedoc/functions/objIsExtensible.html)(); [objIsFrozen](https://nevware21.github.io/ts-utils/typedoc/functions/objIsFrozen.html)(); [objIsSealed](https://nevware21.github.io/ts-utils/typedoc/functions/objIsSealed.html)(); [objKeys](https://nevware21.github.io/ts-utils/typedoc/functions/objKeys.html)(); [objPreventExtensions](https://nevware21.github.io/ts-utils/typedoc/functions/objPreventExtensions.html)(); [objPropertyIsEnumerable](https://nevware21.github.io/ts-utils/typedoc/functions/objPropertyIsEnumerable.html)(); [objSeal](https://nevware21.github.io/ts-utils/typedoc/functions/objSeal.html)(); [objGetPrototypeOf](https://nevware21.github.io/ts-utils/typedoc/functions/objGetPrototypeOf.html)(); [objSetPrototypeOf](https://nevware21.github.io/ts-utils/typedoc/functions/objSetPrototypeOf.html)(); [objToString](https://nevware21.github.io/ts-utils/typedoc/functions/objToString.html)(); [objValues](https://nevware21.github.io/ts-utils/typedoc/functions/objValues.html)();
[polyObjEntries](https://nevware21.github.io/ts-utils/typedoc/functions/polyObjEntries.html)(); [polyObjIs](https://nevware21.github.io/ts-utils/typedoc/functions/polyObjIs.html)(); [polyObjKeys](https://nevware21.github.io/ts-utils/typedoc/functions/polyObjKeys.html)();
+| Object | [deepExtend](https://nevware21.github.io/ts-utils/typedoc/functions/deepExtend.html)(); [forEachOwnKey](https://nevware21.github.io/ts-utils/typedoc/functions/forEachOwnKey.html)(); [forEachOwnKeySafe](https://nevware21.github.io/ts-utils/typedoc/functions/forEachOwnKeySafe.html)(); [isObject](https://nevware21.github.io/ts-utils/typedoc/functions/isObject.html)(); [isUnsafePropKey](https://nevware21.github.io/ts-utils/typedoc/functions/isUnsafePropKey.html)(); [isUnsafeTarget](https://nevware21.github.io/ts-utils/typedoc/functions/isUnsafeTarget.html)(); [objAssign](https://nevware21.github.io/ts-utils/typedoc/functions/objAssign.html)(); [objCopyProps](https://nevware21.github.io/ts-utils/typedoc/functions/objCopyProps.html)(); [objCreate](https://nevware21.github.io/ts-utils/typedoc/functions/objCreate.html)(); [objDeepCopy](https://nevware21.github.io/ts-utils/typedoc/functions/objDeepCopy.html)(); [objDeepFreeze](https://nevware21.github.io/ts-utils/typedoc/functions/objDeepFreeze.html)(); [objDefaults](https://nevware21.github.io/ts-utils/typedoc/functions/objDefaults.html)(); [objDefine](https://nevware21.github.io/ts-utils/typedoc/functions/objDefine.html)(); [objDefineAccessors](https://nevware21.github.io/ts-utils/typedoc/functions/objDefineAccessors.html)(); [objDefineGet](https://nevware21.github.io/ts-utils/typedoc/functions/objDefineGet.html)(); [objDefineProp](https://nevware21.github.io/ts-utils/typedoc/functions/objDefineProp.html)(); [objDefineProps](https://nevware21.github.io/ts-utils/typedoc/functions/objDefineProps.html)(); [objDefineProperties](https://nevware21.github.io/ts-utils/typedoc/functions/objDefineProperties.html)(); [objDiff](https://nevware21.github.io/ts-utils/typedoc/functions/objDiff.html)(); [objEntries](https://nevware21.github.io/ts-utils/typedoc/functions/objEntries.html)(); [objExtend](https://nevware21.github.io/ts-utils/typedoc/functions/objExtend.html)(); [objForEachKey](https://nevware21.github.io/ts-utils/typedoc/functions/objForEachKey.html)(); [objForEachKeySafe](https://nevware21.github.io/ts-utils/typedoc/functions/objForEachKeySafe.html)(); [objFreeze](https://nevware21.github.io/ts-utils/typedoc/functions/objFreeze.html)(); [objFromEntries](https://nevware21.github.io/ts-utils/typedoc/functions/objFromEntries.html)(); [objGetOwnPropertyDescriptor](https://nevware21.github.io/ts-utils/typedoc/functions/objGetOwnPropertyDescriptor.html)(); [objGetOwnPropertyDescriptors](https://nevware21.github.io/ts-utils/typedoc/functions/objGetOwnPropertyDescriptors.html)(); [objGetOwnPropertyNames](https://nevware21.github.io/ts-utils/typedoc/functions/objGetOwnPropertyNames.html)(); [objGetOwnPropertySymbols](https://nevware21.github.io/ts-utils/typedoc/functions/objGetOwnPropertySymbols.html)(); [objHasOwn](https://nevware21.github.io/ts-utils/typedoc/functions/objHasOwn.html)(); [objHasOwnProperty](https://nevware21.github.io/ts-utils/typedoc/functions/objHasOwnProperty.html)(); [objIs](https://nevware21.github.io/ts-utils/typedoc/functions/objIs.html)(); [objIsExtensible](https://nevware21.github.io/ts-utils/typedoc/functions/objIsExtensible.html)(); [objIsFrozen](https://nevware21.github.io/ts-utils/typedoc/functions/objIsFrozen.html)(); [objIsSealed](https://nevware21.github.io/ts-utils/typedoc/functions/objIsSealed.html)(); [objKeys](https://nevware21.github.io/ts-utils/typedoc/functions/objKeys.html)(); [objMapValues](https://nevware21.github.io/ts-utils/typedoc/functions/objMapValues.html)(); [objMergeIf](https://nevware21.github.io/ts-utils/typedoc/functions/objMergeIf.html)(); [objOmit](https://nevware21.github.io/ts-utils/typedoc/functions/objOmit.html)(); [objOmitBy](https://nevware21.github.io/ts-utils/typedoc/functions/objOmitBy.html)(); [objPick](https://nevware21.github.io/ts-utils/typedoc/functions/objPick.html)(); [objPickBy](https://nevware21.github.io/ts-utils/typedoc/functions/objPickBy.html)(); [objPreventExtensions](https://nevware21.github.io/ts-utils/typedoc/functions/objPreventExtensions.html)(); [objPropertyIsEnumerable](https://nevware21.github.io/ts-utils/typedoc/functions/objPropertyIsEnumerable.html)(); [objSeal](https://nevware21.github.io/ts-utils/typedoc/functions/objSeal.html)(); [objGetPrototypeOf](https://nevware21.github.io/ts-utils/typedoc/functions/objGetPrototypeOf.html)(); [objSetPrototypeOf](https://nevware21.github.io/ts-utils/typedoc/functions/objSetPrototypeOf.html)(); [objToString](https://nevware21.github.io/ts-utils/typedoc/functions/objToString.html)(); [objValues](https://nevware21.github.io/ts-utils/typedoc/functions/objValues.html)();
[polyObjEntries](https://nevware21.github.io/ts-utils/typedoc/functions/polyObjEntries.html)(); [polyObjIs](https://nevware21.github.io/ts-utils/typedoc/functions/polyObjIs.html)(); [polyObjKeys](https://nevware21.github.io/ts-utils/typedoc/functions/polyObjKeys.html)();
| String | [asString](https://nevware21.github.io/ts-utils/typedoc/functions/asString.html)(); [getLength](https://nevware21.github.io/ts-utils/typedoc/functions/getLength.html)(); [isString](https://nevware21.github.io/ts-utils/typedoc/functions/isString.html)(); [strCount](https://nevware21.github.io/ts-utils/typedoc/functions/strCount.html)(); [strEndsWith](https://nevware21.github.io/ts-utils/typedoc/functions/strEndsWith.html)(); [strIndexOf](https://nevware21.github.io/ts-utils/typedoc/functions/strIndexOf.html)(); [strIsNullOrEmpty](https://nevware21.github.io/ts-utils/typedoc/functions/strIsNullOrEmpty.html)(); [strIsNullOrWhiteSpace](https://nevware21.github.io/ts-utils/typedoc/functions/strIsNullOrWhiteSpace.html)(); [strLastIndexOf](https://nevware21.github.io/ts-utils/typedoc/functions/strLastIndexOf.html)(); [strLeft](https://nevware21.github.io/ts-utils/typedoc/functions/strLeft.html)(); [strPadEnd](https://nevware21.github.io/ts-utils/typedoc/functions/strPadEnd.html)(); [strPadStart](https://nevware21.github.io/ts-utils/typedoc/functions/strPadStart.html)(); [strRepeat](https://nevware21.github.io/ts-utils/typedoc/functions/strRepeat.html)(); [strReplace](https://nevware21.github.io/ts-utils/typedoc/functions/strReplace.html)(); [strReplaceAll](https://nevware21.github.io/ts-utils/typedoc/functions/strReplaceAll.html)(); [strRight](https://nevware21.github.io/ts-utils/typedoc/functions/strRight.html)(); [strSlice](https://nevware21.github.io/ts-utils/typedoc/functions/strSlice.html)(); [strSplit](https://nevware21.github.io/ts-utils/typedoc/functions/strSplit.html)(); [strStartsWith](https://nevware21.github.io/ts-utils/typedoc/functions/strStartsWith.html)(); [strSubstr](https://nevware21.github.io/ts-utils/typedoc/functions/strSubstr.html)(); [strSubstring](https://nevware21.github.io/ts-utils/typedoc/functions/strSubstring.html)(); [strSymSplit](https://nevware21.github.io/ts-utils/typedoc/functions/strSymSplit.html)(); [strTruncate](https://nevware21.github.io/ts-utils/typedoc/functions/strTruncate.html)(); [strTrim](https://nevware21.github.io/ts-utils/typedoc/functions/strTrim.html)(); [strTrimEnd](https://nevware21.github.io/ts-utils/typedoc/functions/strTrimEnd.html)(); [strTrimLeft](https://nevware21.github.io/ts-utils/typedoc/functions/strTrimLeft.html)(); [strTrimRight](https://nevware21.github.io/ts-utils/typedoc/functions/strTrimRight.html)(); [strTrimStart](https://nevware21.github.io/ts-utils/typedoc/functions/strTrimStart.html)(); [strLetterCase](https://nevware21.github.io/ts-utils/typedoc/functions/strLetterCase.html)(); [strCapitalizeWords](https://nevware21.github.io/ts-utils/typedoc/functions/strCapitalizeWords.html)(); [strCamelCase](https://nevware21.github.io/ts-utils/typedoc/functions/strCamelCase.html)(); [strKebabCase](https://nevware21.github.io/ts-utils/typedoc/functions/strKebabCase.html)(); [strSnakeCase](https://nevware21.github.io/ts-utils/typedoc/functions/strSnakeCase.html)(); [strUpper](https://nevware21.github.io/ts-utils/typedoc/functions/strUpper.html)(); [strLower](https://nevware21.github.io/ts-utils/typedoc/functions/strLower.html)(); [strContains](https://nevware21.github.io/ts-utils/typedoc/functions/strContains.html)(); [strIncludes](https://nevware21.github.io/ts-utils/typedoc/functions/strIncludes.html)();
[polyStrSubstr](https://nevware21.github.io/ts-utils/typedoc/functions/polyStrSubstr.html)(); [polyStrTrim](https://nevware21.github.io/ts-utils/typedoc/functions/polyStrTrim.html)(); [polyStrTrimEnd](https://nevware21.github.io/ts-utils/typedoc/functions/polyStrTrimEnd.html)(); [polyStrTrimStart](https://nevware21.github.io/ts-utils/typedoc/functions/polyStrTrimStart.html)(); [polyStrIncludes](https://nevware21.github.io/ts-utils/typedoc/functions/polyStrIncludes.html)();
| Symbol | [WellKnownSymbols](https://nevware21.github.io/ts-utils/typedoc/enums/WellKnownSymbols.html) (const enum);
[getKnownSymbol](https://nevware21.github.io/ts-utils/typedoc/functions/getKnownSymbol.html)(); [getSymbol](https://nevware21.github.io/ts-utils/typedoc/functions/getSymbol.html)(); [hasSymbol](https://nevware21.github.io/ts-utils/typedoc/functions/hasSymbol.html)(); [isSymbol](https://nevware21.github.io/ts-utils/typedoc/functions/isSymbol.html)(); [newSymbol](https://nevware21.github.io/ts-utils/typedoc/functions/newSymbol.html)(); [symbolFor](https://nevware21.github.io/ts-utils/typedoc/functions/symbolFor.html)(); [symbolKeyFor](https://nevware21.github.io/ts-utils/typedoc/functions/symbolKeyFor.html)();
[polyGetKnownSymbol](https://nevware21.github.io/ts-utils/typedoc/functions/polyGetKnownSymbol.html)(); [polyNewSymbol](https://nevware21.github.io/ts-utils/typedoc/functions/polyNewSymbol.html)(); [polySymbolFor](https://nevware21.github.io/ts-utils/typedoc/functions/polySymbolFor.html)(); [polySymbolKeyFor](https://nevware21.github.io/ts-utils/typedoc/functions/polySymbolKeyFor.html)();

Polyfills are used to automatically backfill runtimes that do not support `Symbol`, not all of the Symbol functionality is provided. | Timer | [createTimeout](https://nevware21.github.io/ts-utils/typedoc/functions/createTimeout.html)(); [createTimeoutWith](https://nevware21.github.io/ts-utils/typedoc/functions/createTimeoutWith.html)(); [elapsedTime](https://nevware21.github.io/ts-utils/typedoc/functions/elapsedTime.html)(); [perfNow](https://nevware21.github.io/ts-utils/typedoc/functions/perfNow.html)(); [setGlobalTimeoutOverrides](https://nevware21.github.io/ts-utils/typedoc/functions/setGlobalTimeoutOverrides.html)(); [setTimeoutOverrides](https://nevware21.github.io/ts-utils/typedoc/functions/setTimeoutOverrides.html)(); [utcNow](https://nevware21.github.io/ts-utils/typedoc/functions/utcNow.html)(); [scheduleIdleCallback](https://nevware21.github.io/ts-utils/typedoc/functions/scheduleIdleCallback.html)(); [scheduleInterval](https://nevware21.github.io/ts-utils/typedoc/functions/scheduleInterval.html)(); [scheduleTimeout](https://nevware21.github.io/ts-utils/typedoc/functions/scheduleTimeout.html)(); [scheduleTimeoutWith](https://nevware21.github.io/ts-utils/typedoc/functions/scheduleTimeoutWith.html)(); [hasIdleCallback](https://nevware21.github.io/ts-utils/typedoc/functions/hasIdleCallback.html)();
For runtimes that don't support `requestIdleCallback` normal setTimeout() is used with the values from [`setDefaultIdleTimeout`](https://nevware21.github.io/ts-utils/typedoc/functions/setDefaultIdleTimeout.html)() and [`setDefaultMaxExecutionTime`](https://nevware21.github.io/ts-utils/typedoc/functions/setDefaultMaxExecutionTime.html)();
[polyUtcNow](https://nevware21.github.io/ts-utils/typedoc/functions/polyUtcNow.html)(); diff --git a/docs/feature-backlog.md b/docs/feature-backlog.md index c6f50510..ec2bb516 100644 --- a/docs/feature-backlog.md +++ b/docs/feature-backlog.md @@ -36,21 +36,7 @@ Notes: - Iterator helpers are intentionally listed as utility suggestions here rather than standard-language mappings. - Implementations should include ES5 polyfills where applicable for v0.x/v1.x compatibility -### A. Object Utilities (Medium Value) - -- `objPick` / `objOmit` -- `objMapValues` -- `objMergeIf` -- `objDiff` -- `objPickBy` / `objOmitBy` -- `objDefaults` for shallow default assignment without overriding defined values - -Notes: - -- maintain plain-object safety patterns -- avoid behavior changes to existing deep copy helpers - -### B. Iterator and Collection Helpers (Medium Value) +### A. Iterator and Collection Helpers (Medium Value) - `iterMap`, `iterFilter`, `iterTake` – Iterator transformation helpers - `iterReduce`, `iterSome`, `iterEvery` – Iterator reduction/testing @@ -58,7 +44,7 @@ Notes: - `arrToMap` helpers with stable key selection - lightweight set operations for iterables -### C. Reliability and Tooling (High Value) +### B. Reliability and Tooling (High Value) - keep bundle-size thresholds justified with measured report - require test parity for polyfill vs native behavior diff --git a/lib/src/index.ts b/lib/src/index.ts index 628d04cb..1b256813 100644 --- a/lib/src/index.ts +++ b/lib/src/index.ts @@ -111,8 +111,8 @@ export { ObjDefinePropDescriptor, ObjDefinePropDescriptorMap, objDefine, objDefineProp, objDefineGet, objDefineAccessors, objDefineProperties, objDefineProps } from "./object/define"; -export { objForEachKey } from "./object/for_each_key"; -export { forEachOwnKeySafe } from "./object/forEachOwnKeySafe"; +export { objForEachKey, objForEachKeySafe } from "./object/for_each_key"; +export { forEachOwnKey, forEachOwnKeySafe } from "./object/forEachOwnKey"; export { isUnsafePropKey } from "./object/isUnsafePropKey"; export { objGetOwnPropertyDescriptor, objGetOwnPropertyDescriptors, objGetOwnPropertyNames, @@ -131,6 +131,10 @@ export { objPreventExtensions, objIsExtensible } from "./object/prevent_extensio export { objPropertyIsEnumerable } from "./object/property_is_enumerable"; export { objSetPrototypeOf } from "./object/set_proto"; export { objIsFrozen, objIsSealed } from "./object/object_state"; +export { objPick, objOmit, objPickBy, objOmitBy } from "./object/pick"; +export { objMapValues } from "./object/map_values"; +export { objMergeIf, objDefaults } from "./object/defaults"; +export { objDiff } from "./object/diff"; export { strCamelCase, strCapitalizeWords, strKebabCase, strLetterCase, strSnakeCase } from "./string/conversion"; export { strCount } from "./string/count"; export { strAt } from "./string/at"; diff --git a/lib/src/internal/unwrapFunction.ts b/lib/src/internal/unwrapFunction.ts index 1a09072b..8ad5ab8a 100644 --- a/lib/src/internal/unwrapFunction.ts +++ b/lib/src/internal/unwrapFunction.ts @@ -59,6 +59,33 @@ export function _unwrapFunctionWithPoly any>(func }; } +/** + * @internal + * @ignore + * Internal helper to convert an expanded function back into an instance `this` function call + * using only the provided class / prototype function or the polyfill. Unlike + * {@link _unwrapFunctionWithPoly}, this helper does not perform any instance-level lookup. + * @param funcName - The function name to call on the first argument passed to the wrapped function + * @param clsProto - The Class or class prototype to use if the function exists. + * @param polyFunc - The function to call if the class / prototype function is not available + * @returns A function which will call the resolved function against the first passed argument and pass on the remaining arguments + */ +/*#__NO_SIDE_EFFECTS__*/ + +export function _unwrapFunctionNoInstWithPoly 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",