Refinement — type predicates
When you write typeof x === "string" or n > 0 you get a boolean back, but TypeScript also
learns something: in the true branch x is a string, in the false branch it is not. A
Refinement<A, B> packages that narrowing into a first-class, reusable value so you can compose,
combine, and pass these checks around like any other function — without losing the type information
at each step.
The problem with scattered type guards
Section titled “The problem with scattered type guards”Projects grow type guards organically. A isNonEmpty guard lives in one file, an isValidEmail
guard in another, and isParsedDate in a third. None of them compose: you cannot combine two
boolean functions and get back a new type guard. Every time you need a stricter check you write a
new one-off guard, and the narrowing only works at the call site.
A Refinement<A, B> solves this by making each check a named, composable value.
What a Refinement is
Section titled “What a Refinement is”At runtime it is just a predicate function returning true or false. At compile time it is a
type predicate: TypeScript narrows the argument to B in every branch where the call returns
true. Because B extends A, every B is also an A — the refinement only narrows, never
widens.
Creating a Refinement with make
Section titled “Creating a Refinement with make”Refinement.make turns any boolean predicate into a typed refinement.
Composing with compose
Section titled “Composing with compose”When you have two refinements where the output of the first is the input of the second, compose
chains them into a single A → C narrowing.
Intersecting with and
Section titled “Intersecting with and”When both refinements apply to the same base type and you need both to hold at once, and produces
a B & C refinement.
Taking either with or
Section titled “Taking either with or”or produces a B | C refinement that passes when at least one of the two checks succeeds.
Bridging to Maybe with toFilter
Section titled “Bridging to Maybe with toFilter”toFilter converts a refinement into an (a: A) => Maybe<B> function, ready to drop into a
pipeline. This is the idiomatic way to turn validation into an Optional presence.
Combining multiple refinements keeps the pipeline linear:
Bridging to Result with toResult
Section titled “Bridging to Result with toResult”toResult converts a refinement into an (a: A) => Result<E, B> function, making validation
failures explicit as typed errors.
This integrates naturally with Result.chain for multi-step validation:
When to use Refinement
Section titled “When to use Refinement”- You want to represent domain invariants (non-empty, positive, valid format) as reusable, composable named types rather than ad-hoc boolean checks.
- You need to combine two or more predicates into a single narrowing (
and,or,compose) without writing a new type guard by hand. - You want to connect a runtime check to
MaybeorResultpipelines usingtoFilterortoResult. - You have a set of related refinements that build on each other (e.g.
NonEmptyString→TrimmedString→SlugString) and want the narrowing to compose automatically.
Use Predicate<A> instead when you need to combine or reuse boolean checks but don’t require
the narrowed type to flow into subsequent operations. Predicate supports using (adapting
checks to richer input types) and all/any for variable-length combinations — operations that
aren’t meaningful for Refinement because they discard the narrowing. Every Refinement<A, B> is
a Predicate<A>, so Predicate.fromRefinement lets you mix both in the same composition.
Keep using plain type guards when the check is truly one-off and will never be composed,
reused, or passed as a value. Wrapping typeof x === "string" in a Refinement is unnecessary
overhead if it is used exactly once in a simple conditional.