Repository F# setup
open Reified
open Reified.Refinements
open Reified.Result
open Reified.Schema.JsonCustomer Id Tutorial
The built-in types cover shapes common to every domain. This tutorial defines one specific to yours, and — just as importantly — shows how to decide whether it should be a type at all.
open Reified
open Reified.Refinements
ReifiedRefinementsDecide whether it earns a type
A refined type is worth defining when the invariant does something for the code that receives it. Ask what becomes total, or what branch disappears:
| Candidate | Verdict |
|---|---|
CustomerId — a positive account number |
Type. Lookup, ordering, and equality all rely on it, and an id of 0 is a bug you want to catch once. |
EmailAddress — matches an email pattern |
Constraint. Nothing downstream is total because of it; you unwrap it to send mail. |
ShippingWeight — positive, and summed across a parcel |
Constraint. The moment you add two of them, F# cannot carry "positive" through, so the type turns every sum into a Result. |
NormalisedName — trimmed and lower-cased |
Neither. That is a transformation, so it belongs in Parse. |
Only the first changes what later code can assume. The second is real validation with no downstream consequence; the third is validation that arithmetic immediately undoes. Both stay constraints on the underlying value:
field _.Email {
constrain Constraint.present
constrain Constraint.email
}Define the type
A refined type is a private wrapper, one canonical projection, and a refinement:
type CustomerId =
private
| CustomerId of int
/// The canonical underlying representation.
member this.Value =
let (CustomerId value) = this
value
override this.ToString() =
string this.Value
module CustomerId =
/// Admission and its reverse projection, packaged together.
let refinement =
Refinement.define
(Constraint.greaterThan 0) // the rule over the underlying int
CustomerId // wrap a value that passed
_.Value // unwrap again, always
let create value = Refinement.create refinement value
let value (input: CustomerId) = input.Value
06-refined_60-tutorials_20-customer-id.md_page.CustomerIdCustomerIdintAn abbreviation for the CLI type . Basic Types
this: CustomerIdValue: CustomerId -> unit -> intvalue: intToString: CustomerId -> unit -> stringstring: 'T -> stringConverts the argument to a string using ToString. For standard integer and floating point values and any type that implements IFormattableToString conversion uses CultureInfo.InvariantCulture. The input value. The converted string. string 'A' // evaluates to "A" string 0xff // evaluates to "255" string -10 // evaluates to "-10"
Value: int06-refined_60-tutorials_20-customer-id.md_page.CustomerIdModulerefinement: Refinement<int,CustomerId>Reified.Refinements.RefinementCreates and applies reusable refinement definitions.
define: Constraint<'underlying> -> ('underlying -> 'refined) -> ('refined -> 'underlying) -> Refinement<'underlying,'refined>Defines a refinement from one constraint, a constructor, and the reverse projection. Compose several rules with Constraint.all before defining, and reach for Constraint.custom when the rule is an arbitrary predicate. Both produce an ordinary constraint, so there is no separate plural or check-taking constructor. type RetryCount = RetryCount of int let retryCount = Refinement.define (Constraint.between 0 10) RetryCount (fun (RetryCount value) -> value)
Reified.ConstraintModuleCreates, executes, composes, and inspects constraints.
greaterThan: 'value -> Constraint<'value>Requires a value strictly greater than the supplied bound. let quantity : Constraint<int> = Constraint.greaterThan 0
_arg1: CustomerIdcreate: int -> Result<CustomerId,Violation>create: Refinement<'underlying,'refined> -> 'underlying -> Result<'refined,Violation>Constructs a refined value, reporting why the raw value was not admitted. type RetryCount = RetryCount of int let retryCount = Refinement.define (Constraint.between 0 10) RetryCount (fun (RetryCount value) -> value) 3 |> Refinement.create retryCount |> Result.mapError Violation.render
value: CustomerId -> intinput: CustomerIdThe case is private, so CustomerId.create is the only way in:
CustomerId.create 42 // Ok
CustomerId.create 0 // Error [ OutOfRange (GreaterThan "0", Some "0") ]
06-refined_60-tutorials_20-customer-id.md_page.CustomerIdModulecreate: int -> Result<CustomerId,Violation>Use Refinement.defineAll when several constraints describe admission, or
Refinement.defineWithCheck for an invariant no built-in constraint describes. See
Define Refined Types for both.
Give it the operations that justify it
This is the step that separates a useful type from a wrapper. CustomerId is a key, so
what it owes callers is lookup and identity, not arithmetic:
module CustomerId =
// ... as above
/// Total: distinct ids stay distinct, so no entry can be lost.
let index (customers: DistinctList<CustomerId>) = DistinctList.toSet customers
/// Total: ids are ordered, so a range of them is an interval.
let range (first: CustomerId) (second: CustomerId) = Interval.between first secondBoth work because the invariant is a fact about the value, not about the moment of construction — and neither involves arithmetic, which is where F# stops being able to carry the invariant for you. If you cannot write an operation like these, that is good evidence the concept should be a constraint instead.
Use it in domain code
let loadCustomer (id: CustomerId) =
// No guard: id is known to be above zero.
repository.load id.ValueKeep the raw type at input and storage boundaries, and the refined type in between.
Give it a schema
Refinement.constraints exposes the same rule to Schema and other interpreters, so a
boundary describes the type without restating it:
open Reified
let customerIdSchema : Schema<CustomerId> =
Schema.int |> Schema.refine CustomerId.refinement
ReifiedcustomerIdSchema: Schema<CustomerId>Reified.Schema`1Describes a typed value's portable structure and construction for schema interpreters. A schema records shape and construction metadata without tying that metadata to input parsing, diagnostics, validation, codecs, UI generation, or workflow execution. Primitive, collection, optional, union, refined, and record declarations all produce Schema<'value>. Record declarations use the schema<'value> { } computation expression. Each field may contain withSchema, constrain, constraints, refine, and validate operations before the declaration finishes with construct or constructResult.
06-refined_60-tutorials_20-customer-id.md_page.CustomerIdReified.SchemaConstruction, composition, parsing, and checking for universal schemas.
int: Schema<int>Describes a 32-bit integer.
(|>): 'T1 -> ('T1 -> 'U) -> 'UApply a function to a value, the value being on the left, the function on the right The argument. The function. The function result. let doubleIt x = x * 2 3 |> doubleIt // Evaluates to 6
refine: Refinement<'raw,'value> -> Schema<'raw> -> Schema<'value>Maps a raw schema through a reusable bidirectional refinement. The smart constructor runs during parsing. Inspection supplies the raw representation during checking and encoding.
06-refined_60-tutorials_20-customer-id.md_page.CustomerIdModulerefinement: Refinement<int,CustomerId>Parsing checks the int, constructs the CustomerId, and reports failures at the field's
path. Encoding projects back through Value. The emitted JSON Schema carries
exclusiveMinimum: 0 because the constraint travelled with the refinement — you did not
write the rule twice.
To resolve the type in a bare field without a withSchema, register it once as described
in Refined Schemas, which works the same
example through the Schema DSL and shows where schema-local constraints fit alongside it.
Next
- Order Totals — the built-in types used in anger.
- Define Refined Types — the full
Refinementreference. - Schema Integration — applying refinements at structured boundaries.
- Refined Schemas — the Schema-side view.