Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions .size-limit.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
},
Expand Down
4 changes: 3 additions & 1 deletion README.md

Large diffs are not rendered by default.

18 changes: 2 additions & 16 deletions docs/feature-backlog.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,29 +36,15 @@ 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
- `iterToArray` for predictable materialization of iterables / iterators
- `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
Expand Down
8 changes: 6 additions & 2 deletions lib/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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";
Expand Down
27 changes: 27 additions & 0 deletions lib/src/internal/unwrapFunction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,33 @@ export function _unwrapFunctionWithPoly<T, P extends (...args: any) => 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<T, P extends (...args: any) => any>(funcName: keyof T, clsProto?: T, polyFunc?: P) {
let clsFn = clsProto ? clsProto[funcName] : NULL_VALUE;

return function(thisArg: any): ReturnType<P> {
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
Expand Down
2 changes: 1 addition & 1 deletion lib/src/object/copy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Comment thread
nev21 marked this conversation as resolved.
import { isPlainObject } from "./is_plain_object";
import { isUnsafeTarget } from "./isUnsafeTarget";

Expand Down
123 changes: 123 additions & 0 deletions lib/src/object/defaults.ts
Original file line number Diff line number Diff line change
@@ -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).
Comment thread
nev21 marked this conversation as resolved.
* @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<T>(
target: T,
source: Record<PropertyKey, any> | 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;
Comment thread
nev21 marked this conversation as resolved.
}
});
}
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
Comment thread
nev21 marked this conversation as resolved.
* 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<T>(target: T, ...sources: Array<Partial<T> | 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;
Comment thread
nev21 marked this conversation as resolved.
Comment thread
nev21 marked this conversation as resolved.
}
});
}
});
}
return target;
}
4 changes: 2 additions & 2 deletions lib/src/object/define.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof Object.getOwnPropertyDescriptor>(ObjClass as any, GET_OWN_PROPERTY_DESCRIPTOR)), _returnNothing));
Expand Down Expand Up @@ -268,7 +268,7 @@ export function objDefineProps<T>(target: T, propDescMap: ObjDefinePropDescripto
});

arrForEach(_objGetOwnPropertySymbols(propDescMap), (sym) => {
if (objPropertyIsEnumerable(propDescMap, sym)) {
if (_objPropertyIsEnumerable(propDescMap, sym)) {
props[sym] = _createProp(propDescMap[sym]);
}
});
Expand Down
61 changes: 61 additions & 0 deletions lib/src/object/diff.ts
Original file line number Diff line number Diff line change
@@ -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<T>`)
* @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<T, U extends Partial<T> = Partial<T>>(base: T, modified: U): Partial<U> {
const result: Partial<U> = objCreate(null);
Comment thread
nev21 marked this conversation as resolved.

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;
}
Loading