@hyperfrontend/ data-utils§
Comprehensive data structure manipulation with circular reference handling and custom class support.
What is @hyperfrontend/data-utils?
@hyperfrontend/data-utils provides industrial-strength utilities for inspecting, transforming, and comparing complex JavaScript data structures. Unlike basic utility libraries that fail on circular references or treat custom classes as generic objects, this library handles self-referential structures safely and allows registration of custom classes (Maps, Sets, domain models) with specialized traversal logic.
The library centers around a powerful traverse() function that recursively walks any data structure with configurable depth control, executing callbacks at each node. Built on this foundation are specialized utilities: deep equality comparison (isIdentical), selective cloning with filtering (selectiveCopy), circular reference detection (hasCircularReference), path-based searches (locateKey, locateText), and structural transformations (renameKey, removeKey, replaceText). An enhanced type system via getType() distinguishes null, arrays, and custom classes beyond native typeof.
Key Features
Identify and locate self-referential structures without stack overflow
Teach utilities how to traverse Maps, Sets, or domain-specific classes with custom operators
Walk nested structures with depth control, callbacks, and early exit support
Compare objects with circular references using position-aware reference tracking
Copy data structures with predicate filtering and circular reference preservation
Locate keys or text values anywhere in nested structures with regex support
Rename keys, remove properties, or replace text throughout data trees
Distinguish null, arrays, and registered classes beyond standard typeof
Zero External Dependencies
Self-contained implementation with no third-party runtime dependencies
Prototype Pollution Protection
Automatically filters
__proto__during cloning operations
Why Use @hyperfrontend/data-utils?
Handle Real-World Data Structures with Circular References
Production applications frequently encounter circular references in DOM nodes, framework state (React, Vue), ORM models with bidirectional relationships, and graph structures. Standard JSON.stringify() throws on circular references, and naive recursive algorithms cause stack overflow. This library detects circular dependencies safely and handles them appropriately - isIdentical() compares structures with matching circular patterns, selectiveCopy() preserves or breaks cycles as needed, and hasCircularReference() validates data before serialization.
Work with Custom Classes Beyond Plain Objects
Generic utility libraries treat all objects identically, failing to traverse Maps, Sets, or custom data structures correctly. If your application uses Map for caching, Set for unique collections, or domain classes (User, Order, Graph nodes), generic utilities miss their internal state. Register custom classes once with registerIterableClass(), defining how to read keys, access values, and create instances - then every utility (traverse, isIdentical, selectiveCopy) automatically handles your custom types.
Deep Equality Without Manual Implementation
Implementing reliable deep equality is deceptively complex - must handle primitives, nested objects, arrays, functions, dates, circular references, and custom classes. Testing frameworks often provide basic deep equality that fails on edge cases. This library's isIdentical() handles all JavaScript types correctly, compares functions by string representation, tracks circular reference positions (not just existence), and works with registered custom classes. Replace brittle manual comparisons with battle-tested equality checking.
Surgical Data Transformations Without Manual Recursion
Common operations like "rename all 'id' keys to '_id'" or "remove all null values from nested config" require recursive tree walking with careful state management. Manual implementations are error-prone (stack overflow, circular references, prototype pollution). Functions like renameKey(), removeKey(), and replaceText() handle recursion, depth control, and edge cases automatically. Transform deeply nested API responses, sanitize user data, or migrate data structures without writing custom traversal logic.
Type-Safe Data Inspection for Runtime Validation
Runtime type validation requires distinguishing null from objects, arrays from plain objects, and custom classes from generic objects - but typeof null === 'object' and typeof [] === 'object'. The getType() function returns precise types ('null', 'array', 'CustomClassName') and integrates with class registration. Use it to build robust runtime validators, safely access properties, or implement multi-method dispatch based on actual runtime types.
Installation
npm install @hyperfrontend/data-utils
Quick Start
Enhanced Type Detection
import { getType } from '@hyperfrontend/data-utils'
getType(null) // 'null' (not 'object')
getType([1, 2, 3]) // 'array' (not 'object')
getType(new Map()) // 'object' (unless registered)
// After registering custom class
class User {}
registerClassTypes(User)
getType(new User()) // 'User'
Deep Equality with Circular References
import { isIdentical, setConfig } from '@hyperfrontend/data-utils'
const obj1 = { a: 1, b: { c: 2 } }
const obj2 = { a: 1, b: { c: 2 } }
isIdentical(obj1, obj2) // true (deep equality)
// Circular references need reference tracking; without it the comparison recurses until the stack overflows
setConfig({ detectCircularReferences: true })
const circular1 = { name: 'node' }
circular1.self = circular1
const circular2 = { name: 'node' }
circular2.self = circular2
isIdentical(circular1, circular2) // true (circular patterns match)
Traverse Data Structures
import { traverse } from '@hyperfrontend/data-utils'
const data = { user: { name: 'Alice', age: 30 }, settings: { theme: 'dark' } }
// Collect all string values
const strings = []
traverse(
data,
(key, value, path, state) => {
if (typeof value === 'string') state.strings.push(value)
},
{ depth: [0, '*'] },
{ strings }
)
// strings: ['Alice', 'dark']
// Search with depth limits
traverse(data, callback, { depth: [0, 2] }, state) // Only 2 levels deep
Selective Deep Cloning
import { selectiveCopy } from '@hyperfrontend/data-utils'
const data = {
user: { id: 1, name: 'Alice', password: 'secret' },
settings: { theme: 'dark' },
}
// Clone without sensitive fields
const { clone: sanitized, skipped } = selectiveCopy(data, {
include: (value, path, key) => key !== 'password',
})
// sanitized: { user: { id: 1, name: 'Alice' }, settings: { theme: 'dark' } }
// skipped: [{ target: 'secret', path: ['user', 'password'], key: 'password', dataType: 'string' }]
// Clone only specific paths
const { clone: partial } = selectiveCopy(data, {
include: (value, path) => path[0] === 'user',
})
// partial: { user: { id: 1, name: 'Alice', password: 'secret' } }
Detect and Locate Circular References
import { hasCircularReference, locateCircularReference } from '@hyperfrontend/data-utils'
const obj = { a: 1 }
obj.self = obj
hasCircularReference(obj) // true
const locations = locateCircularReference(obj)
// locations: [CircularReference { location: { path: ['self'] }, target: { path: [] } }]
// Meaning: path ['self'] references the root object
// One result by default: pass '*' as the second argument to get every cycle in the graph
Search for Keys and Values
import { locateKey, locateText } from '@hyperfrontend/data-utils'
const data = {
user: { userId: 1, name: 'Alice' },
admin: { userId: 2, name: 'Bob' },
}
// Find all paths containing 'userId'
locateKey(data, 'userId')
// [['user', 'userId'], ['admin', 'userId']]
// Find keys matching pattern
locateKey(data, /user/i)
// [['user'], ['user', 'userId'], ['admin', 'userId']]
// Find values containing text
locateText(data, 'Alice')
// [['user', 'name']]
Transform Data In-Place
import { renameKey, removeKey, replaceText } from '@hyperfrontend/data-utils'
const data = { user_id: 1, user_name: 'Alice' }
// Rename keys throughout structure
renameKey(data, 'user_id', 'userId')
// { userId: 1, user_name: 'Alice' }
// Remove keys by pattern
removeKey(data, /^user_/)
// { userId: 1 } (removes keys starting with 'user_')
// Replace text in all string values
replaceText(data, 'Alice', 'Bob')
// { userId: 1, user_name: 'Bob' }
Register Custom Classes
import { registerIterableClass, registerClassTypes } from '@hyperfrontend/data-utils'
class Graph {
nodes = new Map()
addNode(id, value) {
this.nodes.set(id, value)
}
}
// Register as traversable type
registerIterableClass(
Graph,
(graph) => Array.from(graph.nodes.keys()).map(String), // getKeys
(graph, key) => graph.nodes.get(key), // read
(graph, value, key) => graph.nodes.set(key, value), // write
(graph, key) => graph.nodes.delete(key), // remove
() => new Graph() // instantiate
)
// Now all utilities work with Graph instances
const g1 = new Graph()
g1.addNode('a', 1)
const { clone: g2 } = selectiveCopy(g1) // Deep clone works, and g2 is a Graph
isIdentical(g1, g2) // true
API Overview
There is one walker. traverse recurses through anything iterable and calls your callback at every data point with (key, value, path, state, parent), where path is the list of keys that got there and state is an object you hand in and get back. Most of the rest of the package is a callback over it: locateKey, locateText, renameKey, removeKey and replaceText differ only in what their callback does at each node, and all five return the same string[][] of paths they acted on. The factory behind it, createTraversal, is exported too, so a walk with a different admission rule is one call away.
Back references are opt in, because tracking them costs. setConfig with { detectCircularReferences: true } swaps that plain recursion for one that keeps a stack of the references on the path down to the value it is visiting, which is what lets isIdentical compare two self-referential graphs and selectiveCopy rebuild the loops inside its clone. With the flag off both assume an acyclic structure, and a cycle overflows the stack.
Two functions never need the flag, because they set it for the length of their own call: hasCircularReference answers yes or no, and locateCircularReference reports where each cycle was found and what it points back to, one by default and all of them if you pass '*'. Note that selectiveCopy resolves to { clone, skipped }, so the data points it left out stay inspectable instead of vanishing.
Out of the box only arrays and plain objects are traversable; everything else is a leaf the walk stops at. registerIterableClass is how that list grows. Give it a class plus four operators (getKeys, read, write, remove) and an optional instantiate, and from that point every function above reads and writes inside its instances exactly as it does a plain object: getType reports the class name rather than 'object', searches descend into it, and selectiveCopy rebuilds a real instance rather than an object literal. One registration is what turns a Map, a Set or your own domain model into a first-class traversable type.
Every option, callback signature and result type is in the API reference.
Compatibility
Runs on
- Node.js>=18.0.0supported
- Browserssupported
- Web Workerssupported
Ships as
- ESMTree-shakeable
- CJSNode and older bundlers
- IIFEHyperfrontendDataUtils
- UMDHyperfrontendDataUtils
CDN Usage
<!-- unpkg -->
<script src="https://unpkg.com/@hyperfrontend/data-utils"></script>
<!-- jsDelivr -->
<script src="https://cdn.jsdelivr.net/npm/@hyperfrontend/data-utils"></script>
<script>
const { isIdentical, selectiveCopy, getType } = HyperfrontendDataUtils
</script>
Global variable: HyperfrontendDataUtils
Dependencies
None: zero external dependencies.
Architecture Highlights§
Custom types are taught to the library through registerIterableClass, which takes a class plus four operators (getKeys, read, write, remove) and an optional instantiate. Once registered, a class is traversed, compared, and cloned by the same functions that handle plain objects. Circular references are found by tracking the references actually visited, not by capping depth, so a deep but acyclic structure is never mistaken for a loop.
API Reference§
ƒFunctions
Returns
booleanExample
Checking if object contains specific keys
containsKeys({ a: 1, b: 2 }, ['a', 'b']) // true
containsKeys({ a: 1 }, ['a', 'b']) // falseParameters
| Name | Type | Description |
|---|---|---|
§...classRefs | UnknownClass< | One or more class references to deregister. If none provided, all registered classes will be deregistered. |
Example
Deregistering class types
deregisterClassTypes(MyCustomClass)
deregisterClassTypes() // clears allParameters
| Name | Type | Description |
|---|---|---|
§...classRefs | UnknownClass< | The class constructors to deregister |
Example
Deregistering iterable classes
deregisterIterableClass(MyCollection)
deregisterIterableClass() // clears all except Array/ObjectReturns
ConfigExample
Retrieving configuration
const { detectCircularReferences } = getConfig()Parameters
| Name | Type | Description |
|---|---|---|
§target | unknown | The target value to analyze for depth |
Returns
[ number, string[ ][ ]]Example
Getting depth of nested object
getDepth({ a: { b: { c: 1 } } }) // [3, [['a', 'b', 'c']]]Parameters
| Name | Type | Description |
|---|---|---|
§dataType | T | The type of data to get operators for |
Returns
IterableOperatorsExample
Retrieving operators for array type
const ops = getIterableOperators('array')
const keys = ops.getKeys([1, 2, 3]) // ['0', '1', '2']registerIterableClass.Returns
T[ ]Example
Listing registered iterable types
getIterableTypes() // ['array', 'object', ...registered types]Parameters
Returns
string[ ]Example
Extracting keys from object
getKeysFromIterable({ a: 1, b: 2 }, 'object') // ['a', 'b']typeof operator, however, makes distinction between null, array, and object. Also, when classes are registered via registerClass, it checks if objects are instance of any known registered class.Parameters
| Name | Type | Description |
|---|---|---|
§target | unknown | The target to get the data type of. |
Returns
TExample
Determining data types
getType([1, 2]) // 'array'
getType({ a: 1 }) // 'object'
getType(null) // 'null'Parameters
| Name | Type | Description |
|---|---|---|
§target | unknown | The target value to search within |
§pattern | string | RegExp | The string or regular expression pattern to match against key names (defaults to matching all keys)(default: ...) |
§options? | DepthConfig | Optional configuration to control traversal depth |
Returns
string[ ]Example
Finding unique keys by pattern
getUniqueKeys({ a: { b: 1 }, c: { b: 2 } }, 'b') // ['b']
getUniqueKeys(data, /^user/) // keys starting with 'user'getValue< T>( target: unknown, path: [ string, ...string[ ]], defaultValue?: DefaultValueOptions< T>): T
Parameters
| Name | Type | Description |
|---|---|---|
§target | unknown | The target to get the value from. |
§path | [ | The path to the value. |
§defaultValue? | DefaultValueOptions< | The default value to return if the path does not exist or an error occurs. |
Returns
TExample
Retrieving nested values
getValue({ a: { b: 1 } }, ['a', 'b']) // 1
getValue({ a: 1 }, ['x'], { onMissingKey: 0 }) // 0Parameters
| Name | Type | Description |
|---|---|---|
§target | unknown | The value to check for circular references |
Returns
booleanExample
Detecting circular references
const obj = { a: {} }
obj.a.self = obj
hasCircularReference(obj) // true
hasCircularReference({ a: 1 }) // falseParameters
Returns
booleanExample
Comparing values for identity
isIdentical({ a: 1 }, { a: 1 }) // true
isIdentical([1, 2], [1, 2]) // true
isIdentical({ a: 1 }, { a: 2 }) // falseParameters
| Name | Type | Description |
|---|---|---|
§target | unknown | The target to check. |
Returns
booleantrue if the target is iterable, false otherwise.Example
Checking if value is iterable
isIterable([1, 2]) // true
isIterable({ a: 1 }) // true
isIterable('string') // falseParameters
| Name | Type | Description |
|---|---|---|
§dataType | T | The data type to check |
Returns
booleantrue if the data type is iterable, otherwise falseExample
Checking iterable types
isIterableType('array') // true
isIterableType('object') // true
isIterableType('string') // falseParameters
| Name | Type | Description |
|---|---|---|
§text | string | The string to check |
Returns
booleanExample
Validating marker format
isMarker('__$0') // true
isMarker('__$123') // true
isMarker('regular') // falseParameters
Returns
CircularReference[ ]Example
Locating circular references
const obj = { a: {} }
obj.a.self = obj
locateCircularReference(obj) // [CircularReference { location: ['a', 'self'], target: [] }]locateCircularReferenceRecursive( target: unknown, maxResults: number | "*", path: string[ ], stack: ReferenceStack, result: CircularReference[ ], root: boolean): CircularReference[ ]
Parameters
| Name | Type | Description |
|---|---|---|
§target | unknown | The value to search |
§maxResults | number | "*" | Maximum number of results to find |
§path | string[ | Current path in the object graph |
§stack | ReferenceStack | Reference stack for tracking visited objects |
§result | CircularReference[ | Array to collect found circular references |
§root | boolean | Whether this is the root call(default: false) |
Returns
CircularReference[ ]Example
Recursively locating circular references
const stack = referenceStack()
locateCircularReferenceRecursive(obj, 1, [], stack, [], true)Parameters
| Name | Type | Description |
|---|---|---|
§target | unknown | The target value to search within |
§pattern | string | RegExp | The string or regular expression pattern to match against key names |
§options? | DepthConfig | Optional configuration to control traversal depth |
Returns
string[ ][ ]Example
Locating keys in nested structure
locateKey({ a: { b: 1 }, c: { b: 2 } }, 'b') // [['a', 'b'], ['c', 'b']]Parameters
| Name | Type | Description |
|---|---|---|
§target | unknown | The target value to search within |
§pattern | string | RegExp | The string or regular expression pattern to match against text values |
§options? | DepthConfig | Optional configuration to control traversal depth |
Returns
string[ ][ ]Example
Locating text values in structure
locateText({ a: 'hello', b: { c: 'hello' } }, 'hello') // [['a'], ['b', 'c']]Returns
stringExample
Generating unique marker
const tag = marker() // '__$16789012345001234567890'Returns
ReferenceStackExample
Tracking object references
const stack = referenceStack()
stack.add(myObject)
stack.exists(myObject) // true
stack.remove(myObject)
stack.exists(myObject) // falseParameters
| Name | Type | Description |
|---|---|---|
§...classRefs | UnknownClass[ | One or more class references to register. |
Returns
voidExample
Registering custom class types
registerClassTypes(MyCustomClass, AnotherClass)
getType(new MyCustomClass()) // 'MyCustomClass'registerIterableClass< T>( classRef: UnknownClass< T>, getKeys: ( target: T) => string[ ], read: ( target: T, key: unknown) => unknown, write: ( instance: T, value: unknown, key?: unknown) => void, remove: ( instance: T, key: unknown) => void, instantiate: ( ) => T): void
Parameters
| Name | Type | Description |
|---|---|---|
§classRef | UnknownClass< | The class constructor to register |
§getKeys | ( | Function that returns all keys from an instance |
§read | ( | Function to read a value from an instance by key |
§write | ( | Function to write a value to an instance (with optional key) |
§remove | ( | Function to remove a key from an instance |
§instantiate | ( | Factory function to create new instances (defaults to calling constructor)(default: ...) |
Example
Registering custom iterable class
registerIterableClass(
MyMap,
(m) => [...m.keys()],
(m, k) => m.get(k),
(m, v, k) => m.set(k, v),
(m, k) => m.delete(k)
)Parameters
| Name | Type | Description |
|---|---|---|
§target | unknown | The target value to modify |
§pattern | string | RegExp | The string or regular expression pattern to match against key names for removal |
§options? | DepthConfig | Optional configuration to control traversal depth |
Returns
string[ ][ ]Example
Removing keys by pattern
const obj = { a: 1, temp: 2, b: { temp: 3 } }
removeKey(obj, 'temp') // [['temp'], ['b', 'temp']]renameKey( target: unknown, pattern: string | RegExp, name: string, options?: DepthConfig): string[ ][ ]
Parameters
Returns
string[ ][ ]Example
Renaming keys in structure
const obj = { old: 1, nested: { old: 2 } }
renameKey(obj, 'old', 'new') // [['new'], ['nested', 'new']]replaceText( target: unknown, pattern: string | RegExp, text: string, options?: DepthConfig): string[ ][ ]
Parameters
Returns
string[ ][ ]Example
Replacing text in structure
const obj = { a: 'hello world', b: { c: 'hello' } }
replaceText(obj, 'hello', 'hi') // [['a'], ['b', 'c']]Returns
false | DataTypefalse.Example
Comparing structures
sameStructure({ a: 1, b: 2 }, { a: 'x', b: 'y' }) // 'object'
sameStructure({ a: 1 }, { b: 1 }) // falseParameters
Returns
false | TExample
Comparing data types
sameType([1], [2]) // 'array'
sameType({}, []) // falseParameters
| Name | Type | Description |
|---|---|---|
§target | T | The value to clone |
§options? | SelectiveCopyOptions | Configuration options for selective copying |
Returns
SelectiveCopyResult< T>Example
Selective copying with options
const { clone, skipped } = selectiveCopy({ a: 1, b: 2 }, { includeKeys: ['a'] })
// clone = { a: 1 }, skipped = [{ path: ['b'], ... }]selectiveCopyForCircularReferencesRecursive< T>( target: T, path: string[ ], includeKey: SelectiveCopyPredicate, skipFunctions: boolean, recordSkip: DataPointOperation, stack: ReferenceStack, circularRefs: ReferenceLoop[ ], root: boolean): Partial< T>
Parameters
| Name | Type | Description |
|---|---|---|
§target | T | The object to clone |
§path | string[ | The current path in the data structure |
§includeKey | SelectiveCopyPredicate | Predicate function to determine if a key should be included |
§skipFunctions | boolean | Whether to skip function values |
§recordSkip | DataPointOperation | Callback to record skipped data points |
§stack | ReferenceStack | Reference stack for tracking circular references |
§circularRefs | ReferenceLoop[ | Array to store circular reference information |
§root | boolean | Whether this is the root call(default: false) |
Returns
Partial< T>Example
Copying with circular reference handling
selectiveCopyForCircularReferencesRecursive(obj, [], () => true, false, () => {}, referenceStack(), [], true)selectiveCopyRecursive< T>( target: T, path: string[ ], includeKey: SelectiveCopyPredicate, skipFunctions: boolean, recordSkip: DataPointOperation): Partial< T>
Parameters
| Name | Type | Description |
|---|---|---|
§target | T | The object to copy |
§path | string[ | Current path in the object |
§includeKey | SelectiveCopyPredicate | Predicate to determine if a key should be included |
§skipFunctions | boolean | Whether to skip function values |
§recordSkip | DataPointOperation | Callback to record skipped data points |
Returns
Partial< T>Example
Basic recursive copy
selectiveCopyRecursive(obj, [], () => true, false, () => {})Parameters
Example
Configuring settings
setConfig({ detectCircularReferences: true })Parameters
Returns
SExample
Traversing data structure
traverse({ a: { b: 1 } }, (key, value, path) => {
console.log(path.join('.'), '=', value)
})◇Classes
Constructor
new CircularReference( location: [ string, ...string[ ]], target: string[ ])Properties
Methods
readonly toJSON( ): stringJSON.stringify, which calls this in place of the object.Returns
stringtoString producesreadonly toString( ): stringlocation → target, each path joined with keyDelimiter.Returns
stringExample
Creating a circular reference instance
const ref = new CircularReference(['root', 'child'], ['root'])
console.log(ref.depth) // 1◈Interfaces
Properties
samePositionOfOwnProperties:booleanA flag that indicates the API that two values can match only if their properties are in the same order when set to truedetectCircularReferences:booleanA flag that indicates the API that circular references may exist and should keep a tally of reference stack. Turning this flag ON comes at a performance cost, so enable only when necessary.Properties
Properties
Properties
Properties
Properties
Properties
Properties
lastSeen:( reference: unknown) => numberReturns a negative number corresponding to how many iterations ago the reference was registered in the stack relative to the last entry or null when it is not in the stack.remove:( reference: unknown) => voidRemoves a reference from the stack. Walkers call it once the reference's children have been visited, so the stack only ever holds the path from the root to the value being processed.Properties
include?:SelectiveCopyPredicateA condition that is evaluated to determine if data point should be included.exclude?:SelectiveCopyPredicateA condition that is evaluated to determine if data point should be excluded.Properties
Properties
◆Types
type Callback = ( key: string, value: unknown, path: string[ ], state: any, parent: unknown) => voidtype Condition = ( config: TraverseConfig, key: string, value: unknown, path: string[ ], parent: unknown) => booleantype DataPointOperation = ( target: unknown, path: string[ ], key: string, dataType: T) => voidtype DataType = "undefined" | "object" | "boolean" | "number" | "bigint" | "string" | "symbol" | "function" | "null" | "array"type Predicate = ( x: unknown) => booleantype SelectiveCopyPredicate = ( target: unknown, path: string[ ], key: string, dataType: T) => booleantype Traversal = ( target: T, condition: Condition, callback: Callback, options: DepthConfig, state: any) => anytype TraversalArgs = [ Condition, Callback, TraverseConfig, string, string[ ], T, unknown, S]type TraversalCircular = ( condition: Condition, callback: Callback, config: TraverseConfig, key: string, path: string[ ], value: unknown, parent: unknown, state: any, stack: ReferenceStack, root?: boolean) => anytype TraversalNonCircular = ( condition: Condition, callback: Callback, config: TraverseConfig, key: string, path: string[ ], value: unknown, parent: unknown, state: S) => Stype Traverse = ( target: T, callback: Callback, options?: DepthConfig, state?: any) => anytype UnknownClass = ( args: any[ ]) => Ttype UnknownIterable = Iterable< unknown>type UnknownIterableKey = unknown & string●Variables
Related reading§
- How-to
How to fix "Converting circular structure to JSON"
JSON.stringify throws on an object graph with back references, the engine names only one of them, and every edge I remove reveals another.
- Getting started
Getting Started
Set HyperFrontend up and embed a first feature.
- Architecture
Architecture Guide
How the packages fit together.

