stableEquals
Performs robust deep equality comparison with advanced type support and circular reference handling.
Enhanced version of deep equality comparison that handles complex data structures including
circular references, typed arrays, dates, regular expressions, and sparse arrays. Uses
optimized algorithms with visited object tracking to prevent infinite loops while maintaining
high performance for nested structures.
Key Differences from equals:
- Circular Reference Safe: Uses Map-based tracking to handle self-referencing objects
- Advanced Type Support: Proper comparison for Date, RegExp, TypedArrays, ArrayBuffers
- Sparse Array Handling: Correctly distinguishes sparse arrays from dense ones
- Symbol Property Support: Compares symbol-keyed properties using Reflect.ownKeys
- Memory Safety: No risk of stack overflow with deeply nested circular structures
- Performance: ~2x slower than
equalsbut handles much more complex scenarios When to Use stableEquals vs equals:
// Use equals() for:
- Simple objects without circular references
- Performance-critical code paths
- Basic comparison needs
- Objects with only string/number keys
// Use stableEquals() for:
- Objects with potential circular references
- Complex data with Date, RegExp, TypedArrays
- Sparse arrays or arrays with holes
- Objects with symbol properties
- Maximum reliability over performance
Signature
const stableEquals: (left: unknown, right: unknown, omit?: PropertyKey[]) => boolean
Parameters
| Name | Type | Description |
|---|---|---|
left | - | First value to compare |
right | - | Second value to compare |
omit | - | Array of property keys to exclude from comparison (optional) |
Returns
true if values are deeply equal considering all supported types, false otherwise
Examples
Basic equality comparison
import { stableEquals } from '@winglet/common-utils';
// Primitive values with NaN handling
console.log(stableEquals(42, 42)); // true
console.log(stableEquals('hello', 'hello')); // true
console.log(stableEquals(NaN, NaN)); // true (unlike === operator)
console.log(stableEquals(null, undefined)); // false
// Object comparison
const obj1 = { name: 'John', age: 30 };
const obj2 = { name: 'John', age: 30 };
console.log(stableEquals(obj1, obj2)); // true
console.log(obj1 === obj2); // false (different references)
Advanced type support
// Date objects
const date1 = new Date('2023-01-01T12:00:00Z');
const date2 = new Date('2023-01-01T12:00:00Z');
const date3 = new Date('2023-01-01T13:00:00Z');
console.log(stableEquals(date1, date2)); // true (same timestamp)
console.log(stableEquals(date1, date3)); // false (different timestamp)
// RegExp objects
const regex1 = /test/gi;
const regex2 = /test/gi;
const regex3 = /test/g; // different flags
console.log(stableEquals(regex1, regex2)); // true (same pattern and flags)
console.log(stableEquals(regex1, regex3)); // false (different flags)
// TypedArrays and ArrayBuffers
const uint8_1 = new Uint8Array([1, 2, 3, 4]);
const uint8_2 = new Uint8Array([1, 2, 3, 4]);
const uint8_3 = new Uint8Array([1, 2, 3, 5]);
console.log(stableEquals(uint8_1, uint8_2)); // true (same content)
console.log(stableEquals(uint8_1, uint8_3)); // false (different content)
Circular reference handling
// Objects with circular references
const circular1: any = { name: 'parent' };
circular1.self = circular1;
circular1.child = { parent: circular1, name: 'child' };
const circular2: any = { name: 'parent' };
circular2.self = circular2;
circular2.child = { parent: circular2, name: 'child' };
// stableEquals can handle circular references
console.log(stableEquals(circular1, circular2)); // true
// Different circular structures
const different: any = { name: 'different' };
different.self = different;
console.log(stableEquals(circular1, different)); // false
Sparse array comparison
// Sparse arrays (arrays with holes)
const sparse1 = new Array(5);
sparse1[0] = 'first';
sparse1[4] = 'last';
// sparse1[1], sparse1[2], sparse1[3] are holes
const sparse2 = new Array(5);
sparse2[0] = 'first';
sparse2[4] = 'last';
const dense = ['first', undefined, undefined, undefined, 'last'];
console.log(stableEquals(sparse1, sparse2)); // true (same sparse structure)
console.log(stableEquals(sparse1, dense)); // false (sparse vs dense)
// Regular arrays
const arr1 = [1, 2, { nested: true }, [4, 5]];
const arr2 = [1, 2, { nested: true }, [4, 5]];
console.log(stableEquals(arr1, arr2)); // true
Property exclusion
// Exclude specific properties from comparison
const user1 = {
id: 1,
name: 'Alice',
lastLogin: '2023-01-01T10:00:00Z',
sessionId: 'abc123'
};
const user2 = {
id: 1,
name: 'Alice',
lastLogin: '2023-01-01T11:00:00Z', // Different
sessionId: 'xyz789' // Different
};
// Compare excluding volatile fields
console.log(stableEquals(user1, user2)); // false
console.log(stableEquals(user1, user2, ['lastLogin', 'sessionId'])); // true
Complex nested structures
// Deeply nested objects with mixed types
const complex1 = {
metadata: {
created: new Date('2023-01-01'),
pattern: /\w+/g,
tags: ['important', 'user-data']
},
data: {
users: [
{ id: 1, profile: { active: true, scores: new Float32Array([95.5, 87.2]) } },
{ id: 2, profile: { active: false, scores: new Float32Array([78.1, 92.3]) } }
],
summary: {
total: 2,
active: 1,
averageScore: 88.275
}
}
};
const complex2 = {
metadata: {
created: new Date('2023-01-01'),
pattern: /\w+/g,
tags: ['important', 'user-data']
},
data: {
users: [
{ id: 1, profile: { active: true, scores: new Float32Array([95.5, 87.2]) } },
{ id: 2, profile: { active: false, scores: new Float32Array([78.1, 92.3]) } }
],
summary: {
total: 2,
active: 1,
averageScore: 88.275
}
}
};
console.log(stableEquals(complex1, complex2)); // true
Performance with large structures
// Large nested structures
const createLargeStructure = (depth: number, breadth: number) => {
const obj: any = {};
for (let i = 0; i < breadth; i++) {
if (depth > 0) {
obj[`branch_${i}`] = createLargeStructure(depth - 1, breadth);
} else {
obj[`leaf_${i}`] = `value_${i}`;
}
}
return obj;
};
const large1 = createLargeStructure(5, 10); // Deep structure
const large2 = createLargeStructure(5, 10); // Identical structure
console.time('stableEquals-large');
const isEqual = stableEquals(large1, large2);
console.timeEnd('stableEquals-large');
console.log(isEqual); // true
Symbol properties and edge cases
// Objects with symbol properties
const sym1 = Symbol('test');
const sym2 = Symbol('test'); // Different symbol
const obj1 = { regular: 'prop', [sym1]: 'symbol value' };
const obj2 = { regular: 'prop', [sym1]: 'symbol value' };
const obj3 = { regular: 'prop', [sym2]: 'symbol value' };
console.log(stableEquals(obj1, obj2)); // true (same symbol reference)
console.log(stableEquals(obj1, obj3)); // false (different symbol)
// Mixed types comparison
console.log(stableEquals({}, [])); // false (object vs array)
console.log(stableEquals(new Date(), {})); // false (Date vs plain object)
console.log(stableEquals(/regex/, {})); // false (RegExp vs plain object)
Playground
import { stableEquals } from '@winglet/common-utils'; // Primitive values with NaN handling console.log(stableEquals(42, 42)); // true console.log(stableEquals('hello', 'hello')); // true console.log(stableEquals(NaN, NaN)); // true (unlike === operator) console.log(stableEquals(null, undefined)); // false // Object comparison const obj1 = { name: 'John', age: 30 }; const obj2 = { name: 'John', age: 30 }; console.log(stableEquals(obj1, obj2)); // true console.log(obj1 === obj2); // false (different references)
Notes
Enhanced Features over Basic Equals:
- Circular Reference Detection: Prevents infinite loops with visited object tracking
- Advanced Type Support: Handles Date, RegExp, TypedArray, ArrayBuffer, DataView
- Sparse Array Handling: Correctly compares arrays with holes vs dense arrays
- Symbol Property Support: Compares symbol-keyed properties using Reflect.ownKeys
- Memory Optimization: Uses WeakMap for efficient visited object tracking
Comparison Strategy:
- Primitives: Standard equality with special NaN handling (NaN === NaN)
- Dates: Compares internal timestamp values using getTime()
- RegExp: Compares source pattern and flags for complete equality
- TypedArrays: Byte-by-byte comparison using DataView for accuracy
- Objects: Deep recursive comparison with circular reference protection
- Arrays: Index-by-index comparison with sparse array hole detection
Performance Characteristics:
- Time Complexity: O(n) where n is total number of properties/elements
- Space Complexity: O(d + c) where d is depth, c is number of circular references
- Optimization: Early termination, reference equality check, visited object caching
- Memory Efficient: WeakMap-based visited tracking with automatic cleanup
Circular Reference Handling:
- Detection: Uses Map to track visited object pairs during traversal
- Prevention: Returns true if circular reference is detected (assumes structural equality)
- Bidirectional: Tracks both left->right and right->left object relationships
- Memory Safe: No memory leaks from circular reference tracking
Supported Data Types:
- Primitives: string, number, boolean, null, undefined, symbol, bigint
- Objects: Plain objects, arrays (including sparse), functions
- Built-ins: Date, RegExp, Error, Map, Set (reference equality)
- Binary Data: ArrayBuffer, SharedArrayBuffer, TypedArrays, DataView
- Custom Objects: Objects with custom prototypes
Use Cases:
- Testing Frameworks: Deep equality assertions for complex data structures
- State Management: Comparing application state with circular references
- Caching Systems: Determining if cached data matches current data
- Data Validation: Verifying API responses against expected structures
- Change Detection: Identifying modifications in reactive systems
- Configuration Comparison: Comparing complex configuration objects
Limitations:
- Set/Map Comparison: Currently uses reference equality (could be enhanced)
- Function Comparison: Compares by reference only, not implementation
- Custom Class Instances: May not handle specialized comparison logic
- Performance on Extremely Large Objects: May be slower than simple reference checks
- Prototype Chain: Only compares own properties, ignores inherited properties