compare-guard
v1.0.0
Published
Safe, chainable comparison methods for direct values and potentially undefined or null values in nested object paths
Readme
compare-guard
Safe, chainable comparison methods for direct values and potentially undefined or null values in nested object paths.
Features
- ✅ Simple Value Comparisons - Compare strings, numbers, booleans, and more directly
- ✅ Safe Path Resolution - Resolve nested paths without throwing errors
- ✅ Security First - Prototype pollution protection, input validation, and safe property access
- ✅ Deep Equality - Compare objects and arrays deeply with
deepEq() - ✅ Fluent API - Chainable comparison methods
- ✅ Type Safety - Full TypeScript support with type inference
- ✅ Zero Dependencies - Lightweight and fast
- ✅ Multiple Conditions - Combine conditions with
all(),any(), andnone() - ✅ Flexible Paths - Support both dot notation strings and array paths
- ✅ Utility Methods -
isEmpty(),isEmail(),isUrl(),isJson(), and more - ✅ Tree-shakable - ESM + CommonJS support
Installation
npm install compare-guardQuick Start
import { compare, all, any } from 'compare-guard';
// Simple value comparisons (NEW!)
if (compare('hello').eq('hello')) {
console.log('Strings match!');
}
if (compare(42).gt(10)) {
console.log('Number is greater than 10!');
}
if (compare(true).isTruthy()) {
console.log('Value is truthy!');
}
// Nested object path comparisons
const user = {
profile: {
email: '[email protected]',
age: 25,
},
active: true,
};
if (compare(user, 'profile.email').eq('[email protected]')) {
console.log('Email matches!');
}
// Check if path exists
if (compare(user, 'profile.email').exists()) {
console.log('Email exists!');
}
// Multiple conditions
if (all([
compare(user, 'profile.email').isString(),
compare(user, 'profile.age').isNumber(),
compare(user, 'profile.age').gte(18),
compare(user, 'active').isTruthy(),
])) {
console.log('All validations passed!');
}API Reference
compare(value) or compare(obj, path, options?)
Creates a Comparator instance for direct value comparisons or nested object paths.
Simple Value Comparison:
compare(value) // Compare value directlyNested Object Path:
compare(obj, path, options?)Parameters:
value- Direct value to compare (for simple comparisons)obj- The object to traverse (for nested paths)path- Path as string (dot notation) or array['data', 'users', 0, 'name']options- Optional configurationdefaultValue- Default value if path doesn't existthrowOnError- Whether to throw errors (default: false)
Returns: Comparator instance
Examples:
// Simple value comparison
compare('text').eq('text')
compare(42).gt(10)
compare(true).isTruthy()
// Nested object path
compare(obj, 'deep.nested.property').eq('value')
compare(obj, ['data', 'users', 0, 'name']).exists()Path Formats
The library supports multiple path formats:
// Dot notation
compare(obj, 'data.users[0].name')
// Array format
compare(obj, ['data', 'users', 0, 'name'])
// Mixed (in array format)
compare(obj, ['data', 'users', 0, 'profile', 'email'])Comparator Methods
Existence Checks
compare(obj, 'path').exists() // Path exists and is not null/undefined
compare(obj, 'path').isNull() // Value is strictly null
compare(obj, 'path').isUndefined() // Value is undefined
compare(obj, 'path').isNullOrUndefined() // Value is null or undefinedType Checks
compare(obj, 'path').isString() // Value is a string
compare(obj, 'path').isNumber() // Value is a number
compare(obj, 'path').isBoolean() // Value is a boolean
compare(obj, 'path').isArray() // Value is an array
compare(obj, 'path').isObject() // Value is an object (not null/array)Equality Checks
compare(obj, 'path').eq(value) // Strict equality (===)
compare(obj, 'path').neq(value) // Not equal (!==)
compare(obj, 'path').oneOf(...values) // Value is one of the provided values
compare(obj, 'path').notOneOf(...values) // Value is not one of the provided valuesTruthiness Checks
compare(obj, 'path').isTruthy() // Value is truthy
compare(obj, 'path').isFalsy() // Value is falsyNumeric Comparisons
compare(obj, 'path').gt(n) // Greater than
compare(obj, 'path').gte(n) // Greater than or equal
compare(obj, 'path').lt(n) // Less than
compare(obj, 'path').lte(n) // Less than or equal
compare(obj, 'path').between(min, max) // Between min and max (inclusive)String Operations
compare(obj, 'path').matches(regex) // String matches regex pattern
compare(obj, 'path').startsWith(prefix) // String starts with prefix
compare(obj, 'path').endsWith(suffix) // String ends with suffix
compare(obj, 'path').contains(substring) // String contains substringArray Operations
compare(obj, 'path').includes(value) // Array contains value
compare(obj, 'path').lengthGt(n) // Array/string length > n
compare(obj, 'path').lengthLt(n) // Array/string length < n
compare(obj, 'path').lengthEq(n) // Array/string length === nObject Operations
compare(obj, 'path').hasProperty(prop) // Object has property
compare(obj, 'path').hasAllProperties(...props) // Object has all properties
compare(obj, 'path').hasAnyProperty(...props) // Object has any propertyCustom Validation
compare(obj, 'path').validate((value) => {
// Your custom validation logic
return value > 0 && value < 100;
})Deep Equality
compare(obj1).deepEq(obj2) // Deep comparison for objects/arrays
compare(obj, 'nested.data').deepEq({ a: 1, b: 2 })Utility Methods
compare(value).isEmpty() // Empty string, array, object, null, undefined
compare(value).isNotEmpty() // Not empty
compare(value).isPositive() // Positive number
compare(value).isNegative() // Negative number
compare(value).isInteger() // Integer number
compare(value).isFinite() // Finite number (not Infinity/NaN)
compare(value).isDate() // Valid Date object
compare(value).isEmail() // Valid email format
compare(value).isUrl() // Valid URL
compare(value).isJson() // Valid JSON stringString Utilities
compare(value).isBlank() // Empty or whitespace-only string
compare(value).isNumeric() // String contains only digits
compare(value).isAlphaNumeric() // String is alphanumeric
compare(value).isUppercase() // String is all uppercase
compare(value).isLowercase() // String is all lowercase
compare(value).equalsIgnoreCase(str) // Case-insensitive string equalityDate/Time Comparisons
compare(date).isBefore(otherDate) // Date is before another date
compare(date).isAfter(otherDate) // Date is after another date
compare(date).isToday() // Date is today
compare(date).isInPast() // Date is in the past
compare(date).isInFuture() // Date is in the future
compare(date).isSameDay(otherDate) // Dates are the same dayNumber Utilities
compare(value).isEven() // Number is even
compare(value).isOdd() // Number is odd
compare(value).isDivisibleBy(n) // Number is divisible by n
compare(value).isMultipleOf(n) // Number is multiple of n (alias)
compare(value).inRange(min, max) // Number in range (alias for between)Array Utilities
compare(value).isEmptyArray() // Value is empty array
compare(value).hasMinLength(n) // Array/string has min length
compare(value).hasMaxLength(n) // Array/string has max length
compare(value).every(predicate) // All elements match predicate
compare(value).some(predicate) // Any element matches predicateString Format Validators
compare(value).isHexColor() // Valid hex color (#FF0000 or #fff)
compare(value).isUUID() // Valid UUID (v4 format)
compare(value).isIP() // Valid IP address (IPv4/IPv6)
compare(value).isPhone() // Valid phone number formatComparison Helpers
compare(value).isCloseTo(expected, precision) // Number close to expected (floating point)
compare(value).isInstanceOf(Class) // Value is instance of classType Coercion Helpers
compare(value).asString() // Convert to string (empty string for null/undefined)
compare(value).asNumber() // Convert to number (NaN if invalid)
compare(value).asInteger() // Convert to integer (NaN if invalid)Conditional Functions
all(conditions)
Returns true if ALL conditions pass.
all([
compare(obj1, 'path1').eq('value1'),
compare(obj2, 'path2').gt(10),
compare(obj3, 'path3').exists(),
])any(conditions)
Returns true if ANY condition passes.
any([
compare(obj, 'path1').eq('value1'),
compare(obj, 'path2').eq('value2'),
])none(conditions)
Returns true if NONE of the conditions pass.
none([
compare(obj, 'path1').eq('value1'),
compare(obj, 'path2').eq('value2'),
])Usage Examples
Simple Value Comparisons
import { compare, all } from 'compare-guard';
// String comparisons
if (compare('hello').eq('hello')) {
// Strings match
}
if (compare('[email protected]').matches(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)) {
// Valid email format
}
if (compare('password123').lengthGt(8)) {
// Password is long enough
}
// Number comparisons
if (compare(42).gt(10) && compare(42).lt(100)) {
// Number is between 10 and 100
}
if (compare(age).gte(18) && compare(age).lte(120)) {
// Valid age range
}
// Boolean comparisons
if (compare(isActive).isTruthy()) {
// Value is truthy
}
// Type checks
if (compare(value).isString()) {
// Value is a string
}
if (compare(value).isNumber()) {
// Value is a number
}
// Array checks
if (compare([1, 2, 3]).includes(2)) {
// Array contains value
}
if (compare(items).lengthGt(0)) {
// Array is not empty
}
// Multiple conditions
const isValid = all([
compare(email).isString(),
compare(email).matches(/^[^\s@]+@[^\s@]+\.[^\s@]+$/),
compare(age).isNumber(),
compare(age).gte(18),
]);Form Validation
import { compare, all } from 'compare-guard';
const user = {
profile: {
email: '[email protected]',
age: 25,
},
consent: true,
};
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
const isValid = all([
compare(user, 'profile.email').isString(),
compare(user, 'profile.email').matches(emailRegex),
compare(user, 'profile.age').isNumber(),
compare(user, 'profile.age').gte(18),
compare(user, 'consent').isTruthy(),
]);
if (isValid) {
// Process valid user
}API Response Handling
import { compare } from 'compare-guard';
const response = {
data: {
users: [
{ active: true, name: 'Alice' },
],
},
};
// Safe to check nested paths
if (compare(response, 'data.users[0].active').eq(true)) {
console.log('User is active');
}
// Safe even if path doesn't exist
if (compare(response, 'data.users[0].email').exists()) {
console.log('Email exists');
} else {
console.log('Email not found - no error thrown!');
}Configuration Validation
import { compare, all, any } from 'compare-guard';
const config = {
api: {
url: 'https://api.example.com',
timeout: 5000,
retries: 3,
},
features: {
enabled: true,
flags: ['feature1', 'feature2'],
},
};
// Validate all required fields
const isConfigValid = all([
compare(config, 'api.url').isString(),
compare(config, 'api.url').startsWith('https://'),
compare(config, 'api.timeout').isNumber().gte(1000),
compare(config, 'api.retries').isNumber().between(0, 10),
compare(config, 'features.enabled').isBoolean(),
compare(config, 'features.flags').isArray(),
compare(config, 'features.flags').lengthGt(0),
]);
// Check if any feature flag is set
const hasFeatures = any([
compare(config, 'features.flags').includes('feature1'),
compare(config, 'features.flags').includes('feature2'),
compare(config, 'features.flags').includes('feature3'),
]);Default Values
import { compare } from 'compare-guard';
const data = {};
// Use default value if path doesn't exist
const result = compare(data, 'missing.path', { defaultValue: 'default' });
console.log(result.getValue()); // 'default'
// Check with default value
if (compare(data, 'count', { defaultValue: 0 }).gt(10)) {
// This will compare 0 > 10, which is false
}Custom Validation
import { compare, all } from 'compare-guard';
const product = {
price: 99.99,
stock: 50,
};
const isValidProduct = all([
compare(product, 'price').isNumber(),
compare(product, 'price').validate((price) => price > 0 && price < 10000),
compare(product, 'stock').isNumber(),
compare(product, 'stock').gte(0),
]);Complex Nested Structures
import { compare, all } from 'compare-guard';
const order = {
customer: {
id: 123,
address: {
street: '123 Main St',
city: 'New York',
zip: '10001',
},
},
items: [
{ id: 1, quantity: 2, price: 10.99 },
{ id: 2, quantity: 1, price: 5.99 },
],
total: 27.97,
};
// Validate complex structure
const isValidOrder = all([
compare(order, 'customer.id').isNumber(),
compare(order, 'customer.address.zip').isString(),
compare(order, 'customer.address.zip').matches(/^\d{5}$/),
compare(order, 'items').isArray(),
compare(order, 'items').lengthGt(0),
compare(order, 'items[0].quantity').isNumber(),
compare(order, 'items[0].quantity').gt(0),
compare(order, 'total').isNumber(),
compare(order, 'total').gte(0),
]);Mixed Simple and Nested Comparisons
You can mix simple value comparisons with nested object path comparisons:
import { compare, all } from 'compare-guard';
const user = { email: '[email protected]', age: 25 };
const status = 'active';
const isVerified = true;
// Mix simple and nested comparisons
const isValid = all([
// Simple value comparisons
compare(status).oneOf('active', 'inactive', 'pending'),
compare(isVerified).isTruthy(),
// Nested object comparisons
compare(user, 'email').isString(),
compare(user, 'email').matches(/^[^\s@]+@[^\s@]+\.[^\s@]+$/),
compare(user, 'age').isNumber(),
compare(user, 'age').gte(18),
]);Advanced Validation Examples
import { compare, all } from 'compare-guard';
// Form validation with new features
const formData = {
username: 'JohnDoe123',
email: '[email protected]',
age: 25,
phone: '+1-555-123-4567',
password: 'SecurePass123',
birthDate: new Date('1998-01-15'),
tags: ['developer', 'typescript'],
};
const isValid = all([
// String validations
compare(formData.username).isAlphaNumeric(),
compare(formData.username).hasMinLength(3),
compare(formData.username).hasMaxLength(20),
// Email validation
compare(formData.email).isEmail(),
// Number validations
compare(formData.age).isInteger(),
compare(formData.age).inRange(18, 120),
compare(formData.age).isEven(), // Example: require even age
// Phone validation
compare(formData.phone).isPhone(),
// Password strength
compare(formData.password).hasMinLength(8),
compare(formData.password).isNotEmpty(),
compare(formData.password).some((char) => /[A-Z]/.test(char)), // Has uppercase
// Date validations
compare(formData.birthDate).isInPast(),
compare(formData.birthDate).isBefore(new Date()), // Must be before today
// Array validations
compare(formData.tags).isArray(),
compare(formData.tags).hasMinLength(1),
compare(formData.tags).every((tag) => compare(tag).isString()),
]);
// UUID validation
const id = '550e8400-e29b-41d4-a716-446655440000';
if (compare(id).isUUID()) {
// Valid UUID
}
// Hex color validation
const color = '#FF5733';
if (compare(color).isHexColor()) {
// Valid hex color
}
// Floating point comparison
const result = 0.1 + 0.2;
if (compare(result).isCloseTo(0.3, 0.01)) {
// Numbers are close enough (handles floating point precision)
}
// Type coercion
const userInput = '42';
const age = compare(userInput).asInteger();
if (compare(age).isInteger() && compare(age).gte(18)) {
// Valid age
}TypeScript Support
Full TypeScript support with type inference:
import { compare, Path, CompareOptions } from 'compare-guard';
interface User {
name: string;
age: number;
email: string;
}
const user: User = {
name: 'Alice',
age: 30,
email: '[email protected]',
};
// Type-safe path access
const name = compare(user, 'name' as Path);
const age = compare(user, 'age' as Path);Security Features
Prototype Pollution Protection
The library blocks access to dangerous properties (__proto__, constructor, prototype) to prevent prototype pollution attacks:
const obj = { data: 'value' };
// All blocked for security
compare(obj, '__proto__').exists(); // Returns false
compare(obj, 'constructor').exists(); // Returns false
compare(obj, 'prototype').exists(); // Returns false
compare(obj, 'data.__proto__').exists(); // Returns falseInput Validation
- Path length limit: Maximum 10,000 characters
- Path depth limit: Maximum 100 segments
- Type validation: Paths must be strings or arrays
// These will throw errors
normalizePath('a'.repeat(10001)); // Too long
normalizePath(Array(101).fill('a')); // Too deep
normalizePath(null); // Invalid typeSafe Property Access
The library uses Object.prototype.hasOwnProperty.call() to ensure only own properties are accessed, preventing access to inherited properties from the prototype chain.
Error Handling
The library never throws errors for missing properties**. It returns false for invalid comparisons instead:
const obj = {};
// Safe - no error thrown
compare(obj, 'missing.deep.path').exists(); // Returns false
compare(obj, 'missing.path').eq('value'); // Returns false
compare(obj, 'missing.path').gt(10); // Returns falseNote: Input validation errors (invalid path types, excessive length/depth) will throw errors for security reasons.
Performance
- Zero dependencies
- Fast path resolution
- Tree-shakable (only import what you use)
- No runtime overhead for type checks
- Efficient regex patterns for validators
- Minimal memory footprint
Complete Feature List
Core Features
- ✅ Safe nested object path resolution
- ✅ Simple value comparisons
- ✅ Deep equality comparisons
- ✅ Multiple condition combiners (
all(),any(),none())
Type Checks (15+ methods)
isString(),isNumber(),isBoolean(),isArray(),isObject()isNull(),isUndefined(),isNullOrUndefined()isInteger(),isFinite(),isDate()isInstanceOf()
String Operations (15+ methods)
- Basic:
startsWith(),endsWith(),contains(),matches() - Utilities:
isEmpty(),isBlank(),isNumeric(),isAlphaNumeric() - Case:
isUppercase(),isLowercase(),equalsIgnoreCase() - Format:
isEmail(),isUrl(),isJson(),isHexColor(),isUUID(),isIP(),isPhone()
Number Operations (10+ methods)
- Comparisons:
gt(),gte(),lt(),lte(),between(),inRange() - Checks:
isPositive(),isNegative(),isEven(),isOdd() - Math:
isDivisibleBy(),isMultipleOf(),isCloseTo()
Date Operations (6 methods)
isBefore(),isAfter(),isToday(),isInPast(),isInFuture(),isSameDay()
Array Operations (8+ methods)
- Length:
lengthGt(),lengthLt(),lengthEq(),hasMinLength(),hasMaxLength() - Content:
includes(),isEmptyArray(),every(),some()
Object Operations (3 methods)
hasProperty(),hasAllProperties(),hasAnyProperty()
Type Coercion (3 methods)
asString(),asNumber(),asInteger()
Total: 70+ comparison methods!
Browser Support
Works in all modern browsers and Node.js environments:
- Node.js 14+
- Chrome, Firefox, Safari, Edge (latest versions)
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
License
MIT
Related Packages
- lodash.get - Similar path resolution but without comparison methods
- jsonpath - JSONPath implementation
Why compare-guard?
Unlike other packages, compare-guard provides:
- Fluent API - Chainable comparison methods
- Safety First - Never throws errors for missing paths
- Multiple Conditions - Built-in
all(),any(),none()helpers - Type Safe - Full TypeScript support
- Zero Dependencies - Lightweight and fast
Perfect for validation, configuration checking, and safe object traversal!
