A focused collection of JavaScript and TypeScript utilities for validating, inspecting, traversing, comparing, and modifying objects.
The package combines conventional object helpers with strongly typed property-path operations. Property paths may be expressed as dotted strings for ordinary object properties or as exact PropertyKey arrays when symbols, numeric array indexes, or property names containing periods must be preserved.
The package also re-exports the cloning API from klona/full.
- Runtime assertions and TypeScript type guards for objects, records, plain objects, and iterables.
- Typed property-path access with inferred return values.
- Exact string, number, and symbol path segments through
SafeAccessor. - Numeric-only array indexing with ECMAScript array-index validation.
- Symbol-aware property existence checks and traversal.
- Protected object mutation that blocks prototype-pollution paths and ECMAScript well-known symbols.
- Iterative deep freeze, seal, and merge operations.
- Source-driven shallow structural comparison across nested property paths.
- Utilities for getters, setters, prototypes, keys, sizes, and non-empty iterables.
When using the package directly, the utilities are available from:
import {
hasProperty,
isPlainObject,
} from '@typhonjs-utils/object';Within TRL, the utilities are available from:
import {
hasProperty,
isPlainObject,
} from '#runtime/util/object';The package re-exports klona/full, so its cloning function is available from the same module:
import { klona } from '#runtime/util/object';Several functions accept a SafeAccessor:
type SafeAccessor = string | readonly PropertyKey[];Dotted strings are convenient for ordinary nested object properties:
const data = {
user: {
profile: {
name: 'Ada'
}
}
};
safeAccess(data, 'user.profile.name');
// 'Ada'A dotted string is split at every period. It therefore cannot represent a literal property name containing . and cannot index arrays.
safeAccess({ 'user.name': 'Ada' }, 'user.name');
// Attempts to resolve data.user.name, not data['user.name'].
safeAccess({ items: ['a'] }, 'items.0');
// Returns the default value because array indexes require numeric keys.Array accessors preserve every key exactly and support strings, numbers, and symbols:
const metadata = Symbol('metadata');
const data = {
'user.name': 'Ada',
items: ['a', 'b'],
[metadata]: {
active: true
}
};
safeAccess(data, ['user.name']);
// 'Ada'
safeAccess(data, ['items', 1]);
// 'b'
safeAccess(data, [metadata, 'active']);
// trueArray indexes must be numbers in the range 0 through 0xFFFF_FFFE. String indexes, negative numbers, fractional numbers, and values outside the ECMAScript array-index range are rejected.
Inline accessor arrays retain tuple inference through a TypeScript const type parameter:
const result = safeAccess(data, ['items', 0]);
// Inferred from the exact path.A few distinctions are intentional:
safeAccessreturns its default value when the resolved value isundefinedornull.hasPropertychecks whether the complete path exists, so presentundefinedandnullvalues returntrue.- Property lookup includes inherited properties.
- Functions may be traversed as intermediate values by the internal property resolver.
- Empty string and empty array accessors are considered invalid paths.
const data = {
undefinedValue: undefined,
nullValue: null
};
safeAccess(data, 'undefinedValue', 'fallback');
// 'fallback'
hasProperty(data, 'undefinedValue');
// true
hasProperty(data, 'nullValue');
// true
hasProperty(data, 'missing');
// false| Function | Description |
|---|---|
assertObject |
Asserts that a value is a non-null, non-array object while preserving its existing static type. |
assertPlainObject |
Asserts that a value has either Object.prototype or null as its prototype while preserving its existing static type. |
assertRecord |
Asserts that a value is a non-null, non-array object and narrows it to a string-keyed record without discarding its known shape. |
| Function | Description |
|---|---|
isObject |
Returns whether a value is a non-null, non-array object, preserving a known object type when possible. |
isPlainObject |
Returns whether a value is a plain object created with Object.prototype or a null prototype. |
isRecord |
Returns whether a value is a non-null, non-array object and narrows it to Record<string, unknown>. |
isIterable |
Returns whether a non-null object provides Symbol.iterator; primitive strings are intentionally excluded. |
isAsyncIterable |
Returns whether a non-null object provides Symbol.asyncIterator. |
| Function | Description |
|---|---|
hasAccessor |
Checks the object and its prototype chain for a property descriptor containing both a getter and setter. |
hasGetter |
Checks the object and its prototype chain for a getter descriptor. |
hasSetter |
Checks the object and its prototype chain for a setter descriptor. |
hasPrototype |
Determines whether a constructor is, or inherits from, another constructor. |
hasProperty |
Returns whether a complete SafeAccessor path exists, aborting as soon as resolution fails. |
| Function | Description |
|---|---|
safeAccess |
Resolves a dotted string or exact property-key path and returns a typed value or supplied default. |
safeSet |
Sets or updates a value at a property path with optional missing-object creation and mutation hardening. |
safeKeyIterator |
Iteratively yields enumerable leaf paths as readonly PropertyKey arrays. |
safeEqual |
Verifies that every traversed leaf in a source object resolves to the same value in a target object. |
| Function | Description |
|---|---|
deepMerge |
Recursively merges one or more plain source objects into a plain target object in place. |
deepFreeze |
Iteratively freezes an object graph, including values stored in arrays. |
deepSeal |
Iteratively seals an object graph, including values stored in arrays. |
klona |
Deep-clones values through the API re-exported from klona/full. |
| Function | Description |
|---|---|
ensureNonEmptyIterable |
Returns undefined for a missing, invalid, or empty iterable; otherwise returns an iterable containing the peeked first value and remaining values. |
ensureNonEmptyAsyncIterable |
Performs the equivalent operation for async iterables and may lift a synchronous iterable into an async iterable. |
| Function | Description |
|---|---|
objectKeys |
Returns typed enumerable own string keys for an object, or an empty array for an invalid runtime input. |
objectSize |
Returns the size of maps, sets, boxed strings, arrays, and enumerable string-keyed objects according to their supported representation. |
safeAccess provides compile-time inference for known string paths and readonly tuple paths:
const state = {
settings: {
enabled: true
},
entries: [
{ id: 1 },
{ id: 2 }
]
};
const enabled = safeAccess(state, 'settings.enabled');
// boolean
const id = safeAccess(state, ['entries', 1, 'id']);
// number
const missing = safeAccess(state, 'settings.label', 'Unnamed');
// stringThe supplied default is returned when:
datais not a valid object.- The accessor is invalid or empty.
- An intermediate property is missing.
- An intermediate value cannot be traversed.
- An array segment is not a valid numeric array index.
- The final value is
undefinedornull.
Use hasProperty when the distinction between a missing property and a present undefined or null property matters.
hasProperty resolves a path with early termination:
const data = {
user: {
name: undefined
},
entries: ['first']
};
hasProperty(data, 'user.name');
// true
hasProperty(data, 'user.email');
// false
hasProperty(data, ['entries', 0]);
// true
hasProperty(data, ['entries', '0']);
// falseInherited properties are considered available because path resolution uses JavaScript property lookup semantics.
safeSet supports direct assignment and basic arithmetic operations:
const data = {
count: 2,
settings: {
enabled: false
}
};
safeSet(data, 'settings.enabled', true);
safeSet(data, 'count', 3, { operation: 'add' });
data.count;
// 5| Operation | Effect |
|---|---|
set |
Assigns the supplied value. |
set-undefined |
Assigns only when the current value is undefined. |
add |
Applies +=. |
sub |
Applies -=. |
mult |
Applies *=. |
div |
Applies /=. |
The default operation is set.
By default, every intermediate path segment must already resolve to an object:
const data = {};
safeSet(data, 'settings.enabled', true);
// falseSet createMissing to create missing intermediate object entries:
safeSet(data, 'settings.enabled', true, {
createMissing: true
});
// trueMissing entries are created as ordinary objects. The function does not infer that a missing segment should be an array.
safeSet rejects the following string path segments:
__proto__prototypeconstructor
It also rejects ECMAScript well-known symbols, including protocol hooks such as:
Symbol.iteratorSymbol.toPrimitiveSymbol.toStringTag
This prevents prototype-pollution paths and modification of built-in language protocols through the generic mutation API. User-created symbols and symbols produced by Symbol.for remain valid.
Validation occurs during traversal. When createMissing is enabled, earlier missing segments may already have been created before a later blocked or invalid segment causes the operation to return false.
safeKeyIterator performs an iterative depth-first traversal and yields readonly arrays suitable for safeAccess, hasProperty, and safeSet:
const marker = Symbol('marker');
const data = {
user: {
name: 'Ada'
},
entries: ['a', 'b'],
[marker]: true
};
[...safeKeyIterator(data)];
// Example:
// [
// [marker],
// ['entries', 0],
// ['entries', 1],
// ['user', 'name']
// ]The exact order follows the iterator's depth-first traversal rules. Array indexes are emitted immediately when an array property is encountered, preserving the established array ordering.
Behavioral details:
- Enumerable own string and symbol properties are included by default.
- Set
hasOwnOnly: falseto include enumerable inherited properties. - Set
arrayIndex: falseto omit numeric array indexes. - Enumerable symbol properties attached to arrays remain included when
arrayIndexis false. - Functions are excluded as leaf values.
- Objects are traversed recursively.
- Array elements are yielded as indexed leaf paths rather than recursively traversed.
MapandSetentries are not traversed.- Literal string keys containing periods remain unambiguous because paths are arrays.
safeEqual is a source-driven structural comparison rather than a symmetric general-purpose deep-equality function:
safeEqual(
{ settings: { enabled: true } },
{ settings: { enabled: true }, extra: 42 }
);
// trueEvery leaf path produced from source must exist in target and resolve to the same value. Extra target properties are ignored.
Values are compared with strict equality:
- Primitive leaves compare by value and type.
- Object and function leaves compare by reference.
NaNdoes not equalNaN.0and-0compare as equal.- Present
undefinedandnullproperties remain distinct from missing properties.
Arrays are compared by their numeric element paths. Object values stored in arrays are therefore compared by reference rather than recursively by their internal properties.
const shared = { id: 1 };
safeEqual({ entries: [shared] }, { entries: [shared] });
// true
safeEqual({ entries: [{ id: 1 }] }, { entries: [{ id: 1 }] });
// falseOptions:
safeEqual(source, target, {
arrayIndex: false,
hasOwnOnly: false
});arrayIndex: falseignores numeric array elements.hasOwnOnly: falseincludes enumerable inherited properties.
deepMerge mutates and returns the target:
const target = {
settings: {
enabled: false,
mode: 'basic'
}
};
deepMerge(target, {
settings: {
enabled: true
}
});
// {
// settings: {
// enabled: true,
// mode: 'basic'
// }
// }Only plain object inputs are accepted. Nested values are recursively merged when both the source and target values are object literals. Other values, including arrays, replace the target value.
Later source objects take precedence over earlier sources:
deepMerge({}, defaults, userOptions, runtimeOverrides);deepFreeze and deepSeal use iterative traversal, avoiding recursive call-stack growth for deeply nested object graphs.
const frozen = deepFreeze({
settings: {
enabled: true
}
});
Object.isFrozen(frozen);
Object.isFrozen(frozen.settings);
// trueBoth functions accept an optional skipKeys set:
deepFreeze(data, {
skipKeys: new Set(['cache'])
});skipKeys applies to traversed object string keys. The containing object is still frozen or sealed; the referenced child is simply omitted from further traversal through that key.
ensureNonEmptyIterable safely peeks at an iterable:
const values = ensureNonEmptyIterable([1, 2, 3]);
if (values)
{
for (const value of values)
{
// 1, 2, 3
}
}It returns undefined when the input is missing, non-iterable, or empty. For one-shot iterators, the returned iterable preserves the already-consumed first item before continuing with the original iterator.
ensureNonEmptyAsyncIterable provides the same behavior for async iteration:
const values = await ensureNonEmptyAsyncIterable(source);
if (values)
{
for await (const value of values)
{
// ...
}
}Synchronous iterables may also be lifted into an async iterable.
The object predicates intentionally provide different narrowing behavior:
interface Options
{
enabled?: boolean;
}
declare const input: Options | undefined;
if (input && isObject(input))
{
input.enabled;
// Existing Options shape is retained.
}Use:
isObjectwhen a known object type should be preserved.isRecordwhen generic string-keyed access is required.isPlainObjectwhen class instances, arrays, and custom prototypes must be excluded.- The corresponding assertion functions when invalid input should throw instead of returning
false.
Descriptor utilities inspect both the instance and its prototype chain:
class Example
{
#value = 0;
get value(): number
{
return this.#value;
}
set value(value: number)
{
this.#value = value;
}
}
const example = new Example();
hasAccessor(example, 'value');
// true
hasGetter(example, 'value');
// true
hasSetter(example, 'value');
// truehasPrototype operates on constructors:
class Base {}
class Derived extends Base {}
hasPrototype(Derived, Base);
// true- Exact tuple accessors provide the strongest
safeAccessinference. - Runtime-sized
PropertyKey[]accessors resolve tounknownat the type level because their path cannot be determined statically. - Dotted string inference is supported for object properties but intentionally rejects traversal into arrays.
These utilities focus on ordinary JavaScript objects and arrays:
MapandSetentries are not traversed bysafeKeyIteratoror compared bysafeEqual.- Property access may invoke getters and proxy traps.
safeEqualis asymmetric and source-driven.- Array element objects are compared by reference.
safeSetcreates missing intermediate segments as objects, not arrays.- Dotted string paths cannot represent literal periods, symbols, or array indexes.
- The package does not attempt to serialize symbols into string paths.
Use exact property-key arrays whenever correctness across the complete JavaScript property-key space is required.
