Don't pass raw input through your app and hope every caller behaves.
Parse at the edge, reject bad data early, and work with a value that already means something.
That gives you a smaller surface area for bugs and a simpler program inside the boundary.
// raw input in, typed value out
const Email = Schema.String.pipe(
Schema.pattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/),
Schema.brand("Email")
)
type Email = Schema.Schema.Type<typeof Email>
const parsed = Schema.decodeUnknownSync(Email)(input)
// bad input stays bad at the edge
const createUser = (input: unknown) =>
Effect.gen(function* () {
const body = yield* Schema.decodeUnknown(BodySchema)(input)
return yield* Users.create(body.email)
})
// after parsing, the rest of the app gets to assume shape
function sendReceipt(email: Email) {
return Mailer.send(email, "receipt")
}
The point is not to be clever.
It's to turn "maybe valid" into "valid enough to use" before the program gets interesting.
References
- Parse, don’t validate — the original essay
- Parse Dont Validate — a short practical summary
- Design by Contract — invariants as runtime checks
- Contract.Invariant — a concrete contract API