Predicate
Predicate Combinators
Combine simple predicates into complex conditions using logical operators.
Overview
| Combinator | Purpose | Example |
|---|---|---|
and(...preds) | All must be true | and(gt(18), lt(65)) → 18 < age < 65 |
or(...preds) | Any must be true | or(eq('admin'), eq('owner')) |
not(pred) | Invert predicate | not(isEmpty) → isNotEmpty |
xor(...preds) | Exactly one true | xor(isPremium, isTrial) |
always() | Always true | Default/fallback case |
never() | Always false | Filter out all |
Key Properties:
- ✅ Short-circuit:
andstops on first false,orstops on first true - ✅ Type-safe: Predicates of one type combine into a predicate of that type.
andandorkeep what up to three type guards prove, so the result narrows.notreturns a plainPredicate. - ✅ Pure: No side effects, same input = same output
- ✅ Nestable: Combinators can be nested infinitely
and() - Logical AND
Signature
const and = <T>(...predicates: Predicate<T>[]): Predicate<T>Returns true only if all predicates return true. Short-circuits on first false.
Basic Usage
import * as R from 'remeda'
import { and, gte, lt } from '@ontologyio/receta/predicate'
// Age range: 18 ≤ age < 65
const workingAge = and(gte(18), lt(65))
const users = [
{ name: 'Teen', age: 16 },
{ name: 'Adult', age: 30 },
{ name: 'Senior', age: 70 }
]
R.filter(users, (u) => workingAge(u.age))
// => [{ name: 'Adult', age: 30 }]Real-World Examples
E-Commerce Product Filtering
import { and, where, gt, lt, eq } from '@ontologyio/receta/predicate'
interface Product {
id: number
name: string
price: number
category: string
inStock: boolean
rating: number
}
// Available quality products under $200
const affordableQuality = where({
price: and(gt(50), lt(200)),
rating: gt(4.0),
inStock: eq(true)
})
R.filter(products, affordableQuality)
// Electronics on sale that are in stock
const saleElectronics = and<Product>(
where({ category: eq('electronics') }),
where({ inStock: eq(true) }),
where({ price: lt(100) })
)User Permissions
interface User {
role: 'admin' | 'moderator' | 'user'
verified: boolean
accountAgeDays: number
premiumTier: number
}
// Must be verified AND (admin OR experienced moderator)
const canAccessModPanel = and<User>(
where({ verified: eq(true) }),
or<User>(
where({ role: eq<User['role']>('admin') }),
and<User>(
where({ role: eq<User['role']>('moderator') }),
where({ accountAgeDays: gt(90) })
)
)
)
if (canAccessModPanel(currentUser)) {
// Grant access
}API Query Filtering
// GitHub Issues: Open bugs assigned to someone
const openAssignedBugs = where({
state: eq('open'),
assignee: not(isNull),
labels: (labels: string[]) => labels.includes('bug')
})
// With additional filters
const criticalBugs = and(
openAssignedBugs,
where({
labels: (labels: string[]) => labels.includes('critical')
})
)Short-Circuit Behavior
import { and } from '@ontologyio/receta/predicate'
let expensiveCheckRan = false
const cheapCheck = (n: number) => n > 0
const expensiveCheck = (n: number) => {
expensiveCheckRan = true
// Expensive computation here
return n % 2 === 0
}
const combined = and(cheapCheck, expensiveCheck)
combined(-5) // => false
console.log(expensiveCheckRan) // => false (never ran!)
combined(3) // => false
console.log(expensiveCheckRan) // => true (cheapCheck passed)Performance tip: Put fast/cheap checks first!
// ✅ Good: fast check first
and(
eq('active'), // Fast equality check
complexValidation // Expensive computation
)
// ❌ Bad: expensive check first
and(
complexValidation,
eq('active')
)or() - Logical OR
Signature
const or = <T>(...predicates: Predicate<T>[]): Predicate<T>Returns true if any predicate returns true. Short-circuits on first true.
Basic Usage
import { or, eq } from '@ontologyio/receta/predicate'
// Match admin or owner
const isStaff = or(
eq('admin'),
eq('owner'),
eq('moderator')
)
const users = [
{ role: 'admin' },
{ role: 'user' },
{ role: 'owner' }
]
R.filter(users, (u) => isStaff(u.role))
// => admin and ownerReal-World Examples
Order Status Filtering
interface Order {
id: number
status: 'pending' | 'processing' | 'shipped' | 'delivered' | 'cancelled'
amount: number
}
// Active orders (not delivered or cancelled)
const isActive = or(
eq('pending'),
eq('processing'),
eq('shipped')
)
R.filter(orders, (o) => isActive(o.status))
// Or using oneOf (cleaner for multiple values)
import { oneOf } from '@ontologyio/receta/predicate'
const isActive2 = oneOf(['pending', 'processing', 'shipped'])Budget or Premium Tiers
// Either budget (< $50) or premium (> $500)
const budgetOrPremium = or(
lt(50),
gt(500)
)
const products = [
{ name: 'Basic', price: 30 },
{ name: 'Standard', price: 100 },
{ name: 'Premium', price: 600 }
]
R.filter(products, (p) => budgetOrPremium(p.price))
// => Basic ($30) and Premium ($600)Multi-Role Access Control
// Can edit if: owner OR verified admin OR senior moderator
const canEdit = (user: User, resource: Resource) =>
or<User>(
where({ id: eq(resource.ownerId) }),
where({ role: eq('admin'), verified: eq(true) }),
where({ role: eq('moderator'), reputation: gt(1000) })
)(user)not() - Logical NOT
Signature
const not = <T>(predicate: Predicate<T>): Predicate<T>Inverts a predicate's result.
Basic Usage
import { not, isEmpty } from '@ontologyio/receta/predicate'
const names = ['Alice', '', 'Bob', '', 'Charlie']
// Filter non-empty strings
R.filter(names, not(isEmpty))
// => ['Alice', 'Bob', 'Charlie']
// Or use the built-in isNotEmpty
import { isNotEmpty } from '@ontologyio/receta/predicate'
R.filter(names, isNotEmpty)Real-World Examples
Exclude Specific Values
// All orders except cancelled
const orders = [
{ id: 1, status: 'pending' },
{ id: 2, status: 'cancelled' },
{ id: 3, status: 'shipped' }
]
R.filter(orders, (o) => not(eq('cancelled'))(o.status))
// Or use neq
R.filter(orders, (o) => neq('cancelled')(o.status))Invert Complex Conditions
// Users who are NOT (verified AND premium)
const notPremiumVerified = not(
and<User>(
where({ verified: eq(true) }),
where({ tier: eq('premium') })
)
)
// Same as: unverified OR non-premium
const equivalent = or<User>(
where({ verified: eq(false) }),
where({ tier: neq('premium') })
)Filter Out Test Data
interface User {
email: string
name: string
isTestAccount: boolean
}
const realUsers = R.filter(
users,
not(where({ isTestAccount: eq(true) }))
)xor() - Exclusive OR
Signature
const xor = <T>(...predicates: Predicate<T>[]): Predicate<T>Returns true if exactly one predicate returns true.
Basic Usage
import { xor, lt, gt } from '@ontologyio/receta/predicate'
// Numbers outside range 8-12 (< 8 OR > 12, but not both)
const outsideRange = xor(lt(8), gt(12))
const numbers = [1, 5, 10, 15, 20]
R.filter(numbers, outsideRange)
// => [1, 5, 15, 20]
// 10 is excluded (neither < 8 nor > 12)Real-World Examples
Subscription Tiers
interface User {
id: number
premium: boolean
trial: boolean
}
// Valid if premium XOR trial (one but not both)
const validSubscription = xor<User>(
where({ premium: eq(true) }),
where({ trial: eq(true) })
)
const users = [
{ id: 1, premium: true, trial: false }, // ✅ valid
{ id: 2, premium: false, trial: true }, // ✅ valid
{ id: 3, premium: true, trial: true }, // ❌ invalid (both)
{ id: 4, premium: false, trial: false } // ❌ invalid (neither)
]
R.filter(users, validSubscription)
// => users 1 and 2Feature Flags
// Product should have EITHER physical shipping OR digital download
const validProduct = xor<Product>(
where({ requiresShipping: eq(true) }),
where({ isDigital: eq(true) })
)always() / never() - Constant Predicates
Signatures
const always = <T>(): Predicate<T> // Always returns true
const never = <T>(): Predicate<T> // Always returns falseUse Cases
Conditional Filtering
import { always } from '@ontologyio/receta/predicate'
const shouldFilter = config.env === 'production'
const predicate = shouldFilter
? where({ verified: eq(true) })
: always<User>()
R.filter(users, predicate)
// In dev: returns all users
// In prod: returns only verifiedDefault Cases
const getFilterPredicate = (role: string): Predicate<User> => {
switch (role) {
case 'admin': return where({ isAdmin: eq(true) })
case 'moderator': return where({ isModerator: eq(true) })
default: return always<User>() // No filtering
}
}Clear All / Keep All
import { never } from '@ontologyio/receta/predicate'
// Debug mode: don't filter logs
const shouldShowLog = debugMode ? always<Log>() : (log) => log.level === 'error'
// Maintenance mode: block all requests
const canAccess = maintenanceMode ? never<Request>() : checkPermissionsDe Morgan's Laws
Combinators follow logical laws that allow refactoring:
// not(and(A, B)) === or(not(A), not(B))
not(and(isAdult, isVerified))
// Equivalent to:
or(not(isAdult), not(isVerified))
// not(or(A, B)) === and(not(A), not(B))
not(or(isAdmin, isOwner))
// Equivalent to:
and(not(isAdmin), not(isOwner))Example:
// "Not (premium AND verified)" means "free OR unverified"
const notPremiumVerified = not(and(isPremium, isVerified))
const equivalent = or(not(isPremium), not(isVerified))
// Both work the same!Performance Tips
1. Order Matters in and
// ✅ Fast checks first
and(
eq('active'), // O(1) equality
isValidEmail, // Regex check
existsInDatabase // Network call
)
// ❌ Slow checks first
and(
existsInDatabase,
isValidEmail,
eq('active')
)2. or with Likely Cases First
// If 90% of users are regular users:
or(
eq('user'), // 90% chance - stops here
eq('admin'), // 8% chance
eq('superadmin') // 2% chance
)3. Flatten Nested Same Operators
// ❌ Unnecessary nesting
and(
and(checkA, checkB),
and(checkC, checkD)
)
// ✅ Flattened
and(checkA, checkB, checkC, checkD)Common Patterns
Pattern 1: Range Validation
// Value within range
const inRange = (min: number, max: number) =>
and(gte(min), lte(max))
// Or use between()
import { between } from '@ontologyio/receta/predicate'
const inRange2 = between(18, 65) // Cleaner!Pattern 2: Multi-Status Check
// Instead of multiple ORs:
or(eq('pending'), eq('processing'), eq('shipped'))
// Use oneOf:
oneOf(['pending', 'processing', 'shipped'])Pattern 3: Permission Check
const hasPermission = (user: User, permission: string) =>
or<User>(
where({ role: eq('admin') }), // Admins can do anything
(u) => u.permissions.includes(permission) // Or has specific permission
)(user)Pattern 4: Nested Object Filtering
const validOrder = where({
status: oneOf(['pending', 'processing']),
amount: and(gt(0), lt(10000)),
customer: where({
verified: eq(true),
country: oneOf(['US', 'CA', 'UK'])
})
})Testing Combinators
import { describe, it, expect } from 'vitest'
import { and, or, not, eq, gt, lt } from '@ontologyio/receta/predicate'
describe('Predicate combinators', () => {
it('and - all must pass', () => {
const inRange = and(gt(10), lt(20))
expect(inRange(15)).toBe(true)
expect(inRange(5)).toBe(false)
expect(inRange(25)).toBe(false)
})
it('or - any can pass', () => {
const extremes = or(lt(10), gt(90))
expect(extremes(5)).toBe(true)
expect(extremes(95)).toBe(true)
expect(extremes(50)).toBe(false)
})
it('not - inverts result', () => {
const notTen = not(eq(10))
expect(notTen(10)).toBe(false)
expect(notTen(5)).toBe(true)
})
})Next Steps
- Builders - Build structured predicates with
where(),oneOf(),prop() - Type Guards - Use type-narrowing predicates
- Common Patterns - Real-world recipes