A common validation problem is not that a program forgets to check input. It is that the program checks the input, keeps the same weak representation, and then has to remember what was already proved.
Suppose an order accepts a quantity as an integer. The boundary rejects zero and negative values, but every later function still receives an ordinary integer. Nothing in that representation distinguishes a checked quantity from 0, -3, or an integer created somewhere else. The validation happened, but the result of that validation was not captured.
A stronger approach is to parse raw data into a type whose values satisfy the required rule. Downstream code then receives evidence in a useful form: if it has a PositiveQuantity, the positivity check has already succeeded.
This article explains that mental model, how it changes program structure, where its guarantees stop, and when a simpler validation check is enough.
Validation answers a question; parsing produces a value
Consider a function that checks a quantity:
function isValidQuantity(value):
return value > 0The function answers a useful question, but its result is separate from the value it describes:
if isValidQuantity(quantity):
reserveStock(quantity)Inside reserveStock, quantity is still just an integer. If that function is callable from several places, each caller must follow the convention that only positive integers are passed.
Parsing combines the check with construction of a more specific value:
function parsePositiveQuantity(value):
if value <= 0:
return error("quantity must be positive")
return PositiveQuantity(value)The important change is not the function name. It is the output type. Successful parsing changes what the rest of the program is allowed to assume.
A useful mental model is:
Raw representations describe what arrived. Trusted types describe what the program has proved about it.
The word trusted does not mean that the value is correct in every possible sense. It means that the constructor or parser establishes a specific, documented set of properties.
Make the guarantee explicit
A type is useful only when its guarantee is clear.
For PositiveQuantity, a reasonable invariant is:
value is an integer AND value > 0An invariant is a condition that must remain true for every valid instance of the type. If callers can construct PositiveQuantity(-4) directly, the type does not actually preserve that invariant.
The design therefore needs a controlled construction path:
function PositiveQuantity.parse(rawInteger):
if rawInteger <= 0:
return error("quantity must be positive")
return PositiveQuantity.internalCreate(rawInteger)The exact syntax depends on the language. Some languages use private constructors, modules, opaque types, smart constructors, or validated factory functions. The engineering principle is the same: code that receives the trusted type should not need to repeat the checks required to create it.
This is stronger than attaching a comment such as // quantity is positive here. A comment records an expectation. A controlled type makes violating that expectation harder through normal program paths.
Put conversion at the boundary between uncertainty and trust
Parsing is most valuable where weak external representations enter code that wants stronger assumptions.
Imagine an HTTP request containing:
quantity = "12"Several facts are still unknown. The text might not be an integer. The integer might be zero. It might be negative. It might exceed a business limit.
A boundary can resolve those questions in stages:
text "12"
|
| parse integer syntax
v
integer 12
|
| enforce quantity rule
v
PositiveQuantity(12)Each successful step removes uncertainty. If a step fails, it returns an error appropriate to that boundary instead of allowing an invalid value deeper into the program.
Downstream code can now be narrower:
function calculateLineTotal(unitPrice, quantity: PositiveQuantity):
return unitPrice * quantity.valueThe calculation does not need if quantity <= 0. Positivity is a precondition represented by the parameter type.
This does not eliminate all error handling. Stock may be unavailable, prices may change, and persistence may fail. Those are different failure modes and should remain explicit.
Preserve the invariant after construction
A trusted type loses its value if normal operations can silently break its guarantee.
Suppose PositiveQuantity exposes a mutable field:
quantity.value = -5The parser may have done everything correctly, but the invariant no longer holds. A design that relies on the type must either prevent such mutation or ensure that every operation preserves the invariant.
For a small value object, immutability is often the simplest choice. Operations return new checked values rather than modifying the existing value:
function add(a: PositiveQuantity, b: PositiveQuantity):
return PositiveQuantity.internalCreate(a.value + b.value)Here the operation can use the mathematical fact that adding two positive integers produces a positive integer. No new runtime check is needed for positivity, assuming the underlying integer representation cannot overflow into an invalid result.
That last assumption matters. In a language with fixed-width integers and wrapping arithmetic, overflow can invalidate the reasoning. The operation must then use checked arithmetic or otherwise handle the representation’s limits. A type-level invariant is only as strong as the operations that maintain it.
Do not claim guarantees the parser cannot establish
A parser should capture facts it can actually verify.
Consider an email address. A program may be able to establish that input is non-empty and matches the application’s accepted syntax. It cannot infer from syntax alone that the mailbox exists, belongs to the user, or can receive messages.
Naming the result VerifiedEmailAddress would therefore be misleading if no ownership verification occurred. A name such as EmailAddress with a documented syntactic invariant is more defensible.
The same distinction appears with identifiers. Parsing "42" into OrderId(42) can establish that the value has the expected representation. It does not establish that order 42 exists. Existence depends on external state and may change after a lookup.
Keep stable facts in the type. Keep time-dependent or external facts in operations that can fail.
Use different types when states have different permissions
Trusted types become especially useful when one operation requires a stronger fact than another.
Suppose a report can be drafted with any date range, but publishing requires a range whose end date is not before its start date. Keeping both states as the same Report type forces publishing code to inspect fields each time.
Instead, the transition can produce a stronger representation:
function validateForPublishing(draft):
if draft.endDate < draft.startDate:
return error("end date must not precede start date")
return PublishableReport(draft)
function publish(report: PublishableReport):
...Now the publishing operation asks for the state it actually needs.
This technique is useful when the distinction is meaningful across several operations. It is less useful when it creates a large family of nearly identical types for one local condition. Types should clarify the program’s states, not turn every boolean fact into a new abstraction.
Decide which layer owns each check
Parsing into trusted types does not mean every rule belongs in a constructor.
A good candidate is a rule that is intrinsic to the value and should hold everywhere the type is used. Examples include a positive quantity, a non-empty identifier, or a percentage constrained to the application’s documented range.
Context-dependent rules usually belong elsewhere. For example:
quantity > 0can be intrinsic toPositiveQuantity.quantity <= stockAvailabledepends on current inventory.quantity <= customerLimitdepends on a customer and policy.quantity <= carrierLimitmay depend on a shipping method.
Trying to place all four rules inside PositiveQuantity would couple a small value type to changing external state. It would also make the meaning of a valid instance unstable: a quantity could become “invalid” merely because inventory changed.
A useful test is to ask: Should this fact remain true regardless of where this value is used? If yes, it may belong in the type’s invariant. If no, keep it in the operation that has the required context.
Avoid parsing the same value repeatedly
A weak design validates at every layer because no layer can tell whether the previous one already did the work:
controller: check quantity > 0
service: check quantity > 0
model: check quantity > 0Repeated checks can be harmless when they protect genuinely independent boundaries, but repetition often signals that the validated fact is not being carried forward.
With a trusted type, the boundary performs the uncertain conversion once:
controller:
quantity = PositiveQuantity.parse(request.quantity)
if quantity is error:
return badRequest(quantity.error)
service.reserve(productId, quantity)The service can still enforce its own business rules, such as stock availability. It does not need to rediscover that quantity is positive.
This separation also improves error ownership. The boundary can translate parse failures into an input error, while the service reports domain failures using domain terms.
Keep escape hatches visible
Real systems sometimes need to reconstruct trusted values from storage, migrate old data, deserialize messages, or integrate with libraries that cannot use the stronger type directly.
An unchecked constructor may be tempting:
PositiveQuantity.unsafeFromInteger(value)If such an escape hatch exists, treat it as a deliberate breach of the normal guarantee. Keep its scope narrow and make the assumption visible. For example, a persistence adapter might use it only after a database constraint guarantees the same invariant.
Even then, duplicated guarantees can drift. If the application changes the allowed range but the database constraint does not, one side may accept values the other rejects. Prefer a single normal parsing path unless bypassing it has a concrete reason and the alternative guarantee is clear.
Know when a simple check is enough
Not every validated value deserves a dedicated type.
A local helper may be clearer when a condition is checked once and the value does not travel through multiple layers. For example, a small command-line script that reads a retry count, checks it, and immediately uses it may gain little from introducing PositiveRetryCount.
A trusted type becomes more valuable when one or more of these conditions apply:
- the same invariant is checked in several places;
- forgetting the check can cause a meaningful defect;
- the value crosses module or architectural boundaries;
- several operations require the same precondition;
- different states allow different operations;
- primitive values with different meanings are easy to mix up.
The trade-off is additional types, constructors, conversion code, and naming. Use that structure where it removes repeated reasoning from the rest of the program.
Conclusion
Validation is more useful when the program preserves what it learned.
Instead of checking raw data and continuing to pass around the same weak representation, parse it into a type with a clear invariant. Control construction, preserve the invariant through later operations, and require that type where the guarantee matters.
Keep the promise narrow. A trusted type should represent facts the program can establish and maintain, not facts that depend on changing external state.
The practical goal is not to eliminate validation. It is to perform each important check at the right boundary and carry the result forward so downstream code can work with stronger, explicit assumptions.