Predicate — Composable Logic
Boolean checks are the structural foundation of control flow. We write them everywhere: inside
Array.prototype.filter callbacks, authorization gates, and conditional branch guards.
Typically, we write them as ad-hoc, inline lambda functions:
While this works, it is entirely un-composable. If we need to negate this rule — for instance, to
find ineligible users — we are forced to wrap the entire block in !(...). If we want to adapt this
exact check to operate on an Order (e.g., verifying if the customer placing the order is an active
adult) rather than a raw User, we must duplicate and rewrite the logic from scratch.
Our domain rules become scattered, hard to test in isolation, and tightly coupled to specific object shapes.
Predicate<A> solves this. It is a simple type alias for a function that accepts an input A and
returns a boolean:
By representing boolean checks as first-class values, we can name them, negate them, combine them, and adapt them to new shapes point-free.
Predicate vs Refinement: How they compare
Section titled “Predicate vs Refinement: How they compare”Predicate<A> and Refinement<A, B> are closely related. In fact, every Refinement is
structurally a Predicate. However, they serve different purposes in your design:
| Type | Type System Behavior | Primary Use Case |
|---|---|---|
Predicate<A> |
None. The type remains A after the check has evaluated to true. |
Standard domain checks where type narrowing is not needed (e.g., isAdult, isAffordable). |
Refinement<A, B> |
Nrows the type to B inside successful conditional branches. |
Type guards where you must assert a stricter type safety boundary (e.g., isString, isEmail). |
Predicate has a major structural superpower: the using operator. It allows you to adapt simple
primitive checks to work on rich, deeply nested domain objects, which is extremely difficult to
express cleanly with Refinement.
If you need to mix a type guard into a simple boolean chain, you can lift it using
Predicate.from.Refinement.
Negating logic with not
Section titled “Negating logic with not”not inverts a predicate, returning a new Predicate<A> without any complex type-casting overhead:
Combining with and and or
Section titled “Combining with and and or”and and or allow you to combine two predicates over the same input type. Both combinators
short-circuit naturally at runtime: and stops evaluating the moment a check fails, while or
stops the moment a check succeeds.
By chaining and in a pipe, you describe complex rules as a highly readable checklist:
Adapting context: using
Section titled “Adapting context: using”using is the most powerful operator in the Predicate toolbox. It allows you to lift a
Predicate<A> to a Predicate<B> by providing a mapping function B => A.
This allows you to define core, atomic rules on primitive types, and project them onto rich domain objects without rewriting the logic:
By writing your rules once on number and boolean, you keep the core math independent of your
data models. You can reuse isAffordable for checking an Order, a ShippingFee, or a TaxRate
simply by mapping the input with using.
Variable-Length Lists: all and any
Section titled “Variable-Length Lists: all and any”When combining a dynamic list of conditions, chaining and/or can become tedious. For this, we
use all and any.
all constructs a single predicate that requires every check in the array to pass:
any constructs a predicate that passes if at least one check in the array succeeds:
Both combinators short-circuit internally (using Array.prototype.every and
Array.prototype.some), ensuring that downstream checks are skipped as soon as the outcome is
determined.
Problems it solves
Section titled “Problems it solves”- Composing complex search and catalog filters: In ecommerce catalogs and search panels,
filtering criteria often combine multiple rules (such as
inStock AND (isDiscounted OR isFeatured) AND NOT isArchived).Predicate.and,Predicate.or, andPredicate.notbuild dynamic filter pipelines without nested inline boolean expressions. - Authorization and security guard rules: Combining multi-condition permission checks (such as
isAuthenticated AND (hasAdminRole OR isResourceOwner) AND NOT isAccountSuspended) into reusable, testable policy functions. - Projecting atomic checks onto nested domain records: Instead of re-implementing checks across
different object structures,
Predicate.usingallows developers to define simple checks on primitives (such asStr.isNonEmptyorNum.greaterThan(0)) and project them cleanly onto specific entity properties. - Short-circuiting rule evaluations with
allandany: When checking an array of security or domain conditions against a request,Predicate.allandPredicate.anyshort-circuit execution as soon as a passing or failing state is determined, skipping unnecessary downstream evaluations.