Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@

### Bug Fixes

- [#627](https://github.com/nevware21/ts-utils/issues/627) fix(object): `objForEachKey`, `objForEachKeySafe`, `forEachOwnKey` and `forEachOwnKeySafe` no longer abort the enumeration when a property getter throws
- The exception from the getter is caught and the callback is still called for that key with an `undefined` value, so the remaining keys are still visited
- Exceptions thrown by the callback itself are not caught and still propagate
- `polyObjKeys` no longer invokes property getters (so it no longer throws for an object with a throwing getter), matching the native `Object.keys`
- `polyObjEntries` and `polyObjValues` still invoke each getter once and propagate any exception it throws, matching the native `Object.entries` and `Object.values`

# v0.16.0 July 18th, 2026

## Changelog
Expand Down
48 changes: 30 additions & 18 deletions lib/src/object/forEachOwnKey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@

import { arrForEach } from "../array/forEach";
import { isStrictNullOrUndefined } from "../helpers/base";
import { CALL, NULL_VALUE, UNDEF_VALUE } from "../internal/constants";
import { CALL, UNDEF_VALUE } from "../internal/constants";
import { _objForEachKey } from "./for_each_key";
import { objGetOwnPropertySymbols } from "./get_own_property";
import { objHasOwn } from "./has_own";
import { isUnsafePropKey } from "./isUnsafePropKey";
import { _objPropertyIsEnumerable } from "./property_is_enumerable";

Expand All @@ -24,6 +24,10 @@ import { _objPropertyIsEnumerable } from "./property_is_enumerable";
* **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`.
*
* If reading a property value throws (for example a getter that throws an exception), the exception
* is caught and the `callbackFn` is still called for that key with an `undefined` value, so a single
* throwing getter does not stop the iteration of the remaining keys.
* @since 0.14.0
* @group Object
* @typeParam T - The source type.
Expand Down Expand Up @@ -65,22 +69,24 @@ import { _objPropertyIsEnumerable } from "./property_is_enumerable";
* ```
*/
export function forEachOwnKey<T>(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;
}
}
}
if (!isStrictNullOrUndefined(theObject)) {
// Resolve the `this` value once, reusing the thisArg variable to keep the minified output small
thisArg = isStrictNullOrUndefined(thisArg) ? theObject : thisArg;
if (_objForEachKey(theObject, callbackfn, thisArg) !== -1) {
arrForEach(objGetOwnPropertySymbols(theObject), (key) => {
if (_objPropertyIsEnumerable(theObject, key)) {
let value: any = UNDEF_VALUE;
try {
value = theObject[key as keyof T];
} catch (e) {
// A throwing getter, so just pass undefined as the value
}

arrForEach(objGetOwnPropertySymbols(theObject), (key) => {
if (_objPropertyIsEnumerable(theObject, key)) {
if (callbackfn[CALL](isStrictNullOrUndefined(thisArg) ? theObject : thisArg, key, theObject[key as keyof T]) === -1) {
return -1;
// arrForEach stops when -1 is returned
return callbackfn[CALL](thisArg, key, value);
}
}
});
});
}
}
}

Expand All @@ -92,6 +98,9 @@ export function forEachOwnKey<T>(theObject: T, callbackfn: (key: PropertyKey, va
* 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).
*
* As with {@link forEachOwnKey}, if reading a property value throws (for example a throwing getter)
* the `callbackFn` is called for that key with an `undefined` value and the iteration continues.
* @since 0.14.0
* @group Object
* @typeParam T - The source type.
Expand Down Expand Up @@ -145,9 +154,12 @@ export function forEachOwnKey<T>(theObject: T, callbackfn: (key: PropertyKey, va
* ```
*/
export function forEachOwnKeySafe<T>(theObject: T, callbackfn: (key: PropertyKey, value: T[keyof T]) => void | number, thisArg?: any): void {
// Resolve the `this` value once (not per key), passing it directly rather than via the wrapper's own `this`
// as that would box primitive values (e.g. 0, "", false) when running in non-strict mode
thisArg = isStrictNullOrUndefined(thisArg) ? theObject : thisArg;
forEachOwnKey(theObject, (key, value) => {
if (!isUnsafePropKey(key)) {
return callbackfn[CALL](isStrictNullOrUndefined(thisArg) ? theObject : thisArg, key, value);
return callbackfn[CALL](thisArg, key, value);
}
}, thisArg);
});
}
61 changes: 51 additions & 10 deletions lib/src/object/for_each_key.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,39 @@
*/

import { isFunction, isObject, isStrictNullOrUndefined } from "../helpers/base";
import { CALL } from "../internal/constants";
import { CALL, UNDEF_VALUE } from "../internal/constants";
import { objHasOwn } from "./has_own";
import { isUnsafePropKey } from "./isUnsafePropKey";

/**
* @internal
* @ignore
* Shared implementation for {@link objForEachKey} and {@link forEachOwnKey}, iterates the own enumerable
* string keys of the object via a for...in loop. Each value is read inline within a try/catch (rather than
* via a helper function call per key) so that a throwing getter passes `undefined` as the value instead of
* aborting the iteration, while exceptions thrown by the callback still propagate.
* @param theObject - The object to iterate over
* @param callbackfn - The callback to call for each key, returning -1 stops the iteration
* @param ctx - The already resolved `this` value to use when calling the callback
* @returns -1 if the callback requested the iteration to stop, otherwise undefined
*/
export function _objForEachKey<T>(theObject: T, callbackfn: (key: string, value: T[keyof T]) => void | number, ctx: any): number | void {
Comment thread
nev21 marked this conversation as resolved.
Resolved
for (const prop in theObject) {
if (objHasOwn(theObject, prop)) {
let value: any = UNDEF_VALUE;
try {
value = theObject[prop as keyof T];
} catch (e) {
// A throwing getter, so just pass undefined as the value
Comment thread
nev21 marked this conversation as resolved.
Resolved
}

if (callbackfn[CALL](ctx, prop, value) === -1) {
return -1;
}
}
}
}

/**
* 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
Expand All @@ -19,6 +48,10 @@ import { isUnsafePropKey } from "./isUnsafePropKey";
* **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.
*
* If reading a property value throws (for example a getter that throws an exception), the exception is caught and the
* `callbackFn` is still called for that key with an `undefined` value, so a single throwing getter does not stop the
* iteration of the remaining keys.
*
* 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
Expand Down Expand Up @@ -54,19 +87,21 @@ import { isUnsafePropKey } from "./isUnsafePropKey";
* console.log(key, value); // name Bob (unsafe keys filtered)
* });
* ```
* @example
* ```ts
* // A throwing getter does not stop the iteration
* const obj = { a: 1, get b(): number { throw new Error("fail"); }, c: 3 };
* objForEachKey(obj, (key, value) => {
* console.log(key, value); // a 1, b undefined, c 3
* });
* ```
* @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<T>(theObject: T, callbackfn: (key: string, value: T[keyof T]) => void | number, thisArg?: any): void {
if (theObject && (isObject(theObject) || isFunction(theObject))) {
for (const prop in theObject) {
if (objHasOwn(theObject, prop)) {
if (callbackfn[CALL](isStrictNullOrUndefined(thisArg) ? theObject : thisArg, prop, theObject[prop as keyof T]) === -1) {
break;
}
}
}
_objForEachKey(theObject, callbackfn, isStrictNullOrUndefined(thisArg) ? theObject : thisArg);
}
}

Expand All @@ -76,6 +111,9 @@ export function objForEachKey<T>(theObject: T, callbackfn: (key: string, value:
*
* 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.
*
* As with {@link objForEachKey}, if reading a property value throws (for example a throwing getter) the `callbackFn`
* is called for that key with an `undefined` value and the iteration continues.
* @group Object
* @since 0.14.0
* @typeParam T - The object type
Expand Down Expand Up @@ -114,9 +152,12 @@ export function objForEachKey<T>(theObject: T, callbackfn: (key: string, value:
* ```
*/
export function objForEachKeySafe<T>(theObject: T, callbackfn: (key: string, value: T[keyof T]) => void | number, thisArg?: any): void {
// Resolve the `this` value once (not per key), passing it directly rather than via the wrapper's own `this`
// as that would box primitive values (e.g. 0, "", false) when running in non-strict mode
thisArg = isStrictNullOrUndefined(thisArg) ? theObject : thisArg;
objForEachKey(theObject, (key: string, value: T[keyof T]) => {
if (!isUnsafePropKey(key)) {
return callbackfn[CALL](isStrictNullOrUndefined(thisArg) ? theObject : thisArg, key, value);
return callbackfn[CALL](thisArg, key, value);
}
}, thisArg);
});
}
29 changes: 20 additions & 9 deletions lib/src/polyfills/object/objKeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,28 @@
* Licensed under the MIT license.
*/

import { isObject } from "../../helpers/base";
import { isFunction, isObject } from "../../helpers/base";
import { dumpObj } from "../../helpers/diagnostics";
import { throwTypeError } from "../../helpers/throw";
import { _throwIfNullOrUndefined } from "../../internal/throwIf";
import { objForEachKey } from "../../object/for_each_key";
import { objHasOwn } from "../../object/has_own";

function _objIterateEntries<T, R>(obj: any, mapper: (key: string, value: T) => R): R[] {
/**
* Iterates the own enumerable string keys of the object, passing only the key to the mapper. This intentionally
* does not use objForEachKey (which reads every value and swallows getter exceptions) so that, like the native
* implementations, polyObjKeys never invokes getters and polyObjEntries / polyObjValues invoke each getter once
* and propagate any exception it throws.
*/
function _objIterateEntries<R>(obj: any, mapper: (key: string) => R): R[] {
const result: R[] = [];
objForEachKey(obj, (key, val) => {
result.push(mapper(key, val));
});
if (obj && (isObject(obj) || isFunction(obj))) {
for (const key in obj) {
if (objHasOwn(obj, key)) {
result.push(mapper(key));
}
}
}

return result;
}

Expand All @@ -35,7 +46,7 @@ export function polyObjKeys(obj: any): string[] {
throwTypeError("non-object " + dumpObj(obj));
}

return _objIterateEntries<any, any>(obj, (k, _) => k);
return _objIterateEntries<string>(obj, (k) => k);
}

/**
Expand All @@ -61,7 +72,7 @@ export function polyObjKeys(obj: any): string[] {
*/
/*#__NO_SIDE_EFFECTS__*/
export function polyObjEntries<T = any>(value: {} | { [s: string]: T } | ArrayLike<T>): [string, T][] {
return _objIterateEntries<T, [string, T]>(value, (k, v) => [k, v]);
return _objIterateEntries<[string, T]>(value, (k) => [k, (value as any)[k]]);
}

/**
Expand All @@ -87,5 +98,5 @@ export function polyObjEntries<T = any>(value: {} | { [s: string]: T } | ArrayLi
*/
/*#__NO_SIDE_EFFECTS__*/
export function polyObjValues<T = any>(value: {} | { [s: string]: T } | ArrayLike<T>): T[] {
return _objIterateEntries<T, T>(value, (_, v) => v);
return _objIterateEntries<T>(value, (k) => (value as any)[k]);
}
4 changes: 2 additions & 2 deletions lib/test/bundle-size-check.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,13 @@ const configs = [
{
name: "es6-min-zip",
path: "../bundle/es6/umd/ts-utils.min.js",
limit: 15.5 * 1024, // 15.5 kb in bytes
limit: 15.75 * 1024, // 15.75 kb in bytes
compress: true
},
{
name: "es5-min-poly",
path: "../bundle/es5/ts-polyfills-utils.min.js",
limit: 10.5 * 1024, // 10.5 kb in bytes
limit: 10.75 * 1024, // 10.75 kb in bytes
compress: false
}
];
Expand Down
Loading
Loading