Predicate — Composable Logic
185 B minified + gzipped
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 object shapes directly inside pipelines without writing extra wrapper functions.
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:
Predicate<A>tests whether a value satisfies a domain rule (such asisAdultorisAffordable), keeping the TypeScript type asAafter the check evaluates totrue.Refinement<A, B>is a type guard ((a: A) => a is B) that narrows the compile-time type ofAto a more specific subtypeBinside conditional branches (such as narrowingunknowntostringorstringto a brandedEmail).
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 difficult to express
cleanly with Refinement.
If you have an existing Refinement<A, B> and want to compose it alongside plain boolean rules
using Predicate.and, Predicate.or, or Predicate.all, lift it with 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 on the first decisive element in the loop, ensuring that downstream checks are skipped as soon as the outcome is determined.
Declarative Conditional Branching: match
Section titled “Declarative Conditional Branching: match”When routing an input value across multiple mutually exclusive boolean rules, nested if / else if
statements or chained ternaries become difficult to scan. Predicate.match accepts an array of
[predicate, handler] pairs and a mandatory fallback handler, evaluating predicates in order and
returning the result of the first matching branch:
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.is.nonEmptyorNum.is.positive) 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.