Validation
55 exports from @ontologyio/receta/validation.
Everything exported from @ontologyio/receta/validation. New here? Start with the Validation guide.
Functions
alphanumeric
Validates string contains only alphanumeric characters.
function alphanumeric(errorMessage?: string | undefined): Validator<string, string, string>alphanumeric()('John123') // => Valid('John123')
alphanumeric()('John@123') // => Invalid(['Must contain only letters and numbers'])between
Validates number is within a range.
function between(minimum: number, maximum: number, errorMessage?: string | undefined): Validator<number, number, string>between(0, 100)(50) // => Valid(50)
between(0, 100)(-1) // => Invalid(['Must be between 0 and 100'])
between(0, 100)(150) // => Invalid(['Must be between 0 and 100'])collectErrors
Collects an array of Validations into a single Validation, accumulating all errors.
This is the key difference from collect in the Result module, which short-circuits on the first error.
collectErrors continues through all validations and accumulates every error found.
If all Validations are Valid, returns Valid with an array of all values. If any Validation is Invalid, returns Invalid with ALL accumulated errors. Every Validation in the array must hold the same value type. To mix types, use schema.
function collectErrors<T, E>(validations: readonly Validation<T, E>[]): Validation<T[], E>function collectErrors<T>(): <E>(validations: readonly Validation<T, E>[]) => Validation<T[], E>// All successful
collectErrors([valid(1), valid(2), valid(3)])
// => Valid([1, 2, 3])
// Accumulates ALL errors (vs collect from Result, which stops at first)
collectErrors([
invalid(['error1']),
invalid(['error2']),
invalid(['error3'])
])
// => Invalid(['error1', 'error2', 'error3'])
// Mixed: accumulates all errors, ignores values
collectErrors([
valid(1),
invalid(['error1']),
valid(2),
invalid(['error2'])
])
// => Invalid(['error1', 'error2'])
// Real-world: Validate all form fields and show all errors
const validateForm = (data: SignupForm) =>
collectErrors([
validateName(data.name),
validateEmail(data.email),
validateCountry(data.country),
validatePassword(data.password)
])
// Returns all validation errors, not just the first one
// Real-world: Bulk validation with full error reporting
const validateUsers = (users: unknown[]) =>
collectErrors(users.map(validateUser))
// Get all invalid users with their errorsSee also validate, validateAll.
defined
Validates value is not null or undefined.
function defined<T>(errorMessage: string): Validator<T | null | undefined, T, string>defined('Value required')(42) // => Valid(42)
defined('Value required')(null) // => Invalid(['Value required'])
defined('Value required')(undefined) // => Invalid(['Value required'])Validates email address format.
Uses a simple but effective regex pattern for email validation.
function email(errorMessage?: string | undefined): Validator<string, string, string>email()('john@example.com') // => Valid('john@example.com')
email()('invalid-email') // => Invalid(['Invalid email address'])
email('Bad email')('invalid') // => Invalid(['Bad email'])equals
Validates value equals expected value.
function equals<T>(expected: T, errorMessage?: string | undefined): Validator<T, T, string>equals('yes')('yes') // => Valid('yes')
equals('yes')('no') // => Invalid(['Must equal: yes'])
// Useful for checkbox confirmations
equals(true, 'You must agree')finite
Validates number is finite (not Infinity or -Infinity).
function finite(errorMessage?: string | undefined): Validator<number, number, string>finite()(42) // => Valid(42)
finite()(Infinity) // => Invalid(['Must be a finite number'])
finite()(-Infinity) // => Invalid(['Must be a finite number'])flatMap
Chains validation operations.
If the validation is Valid, applies the function which returns a new Validation. If the validation is Invalid, returns it unchanged.
This is the monadic bind operation for Validation, allowing you to sequence dependent validations.
function flatMap<T, U, E>(validation: Validation<T, E>, fn: (value: T) => Validation<U, E>): Validation<U, E>function flatMap<T, U, E>(fn: (value: T) => Validation<U, E>): (validation: Validation<T, E>) => Validation<U, E>// Data-first
flatMap(valid(5), x => valid(x * 2)) // => Valid(10)
flatMap(invalid(['error']), x => valid(x * 2)) // => Invalid(['error'])
// Data-last (in pipe)
pipe(
valid(5),
flatMap(x => valid(x * 2))
) // => Valid(10)
// Real-world: Chain dependent validations
const validateAge = (age: number) =>
age >= 18 ? valid(age) : invalid('Must be 18+')
const validateAgeRange = (age: number) =>
age <= 120 ? valid(age) : invalid('Invalid age')
const validateUserAge = (age: number) =>
pipe(
validateAge(age),
flatMap(validateAgeRange)
)
// Parse then validate
const parseAndValidate = (str: string) =>
pipe(
tryCatch(() => JSON.parse(str)),
flatMap(validateUserSchema)
)See also map, flatten.
flatten
Flattens a nested Validation<Validation<T, E>, E> to Validation<T, E>.
If the outer validation is Valid containing a Valid, returns the inner Valid. If the outer validation is Valid containing an Invalid, returns the inner Invalid. If the outer validation is Invalid, returns it unchanged.
function flatten<T, E>(validation: Validation<Validation<T, E>, E>): Validation<T, E>function flatten<T, E>(): (validation: Validation<Validation<T, E>, E>) => Validation<T, E>// Data-first
flatten(valid(valid(5))) // => Valid(5)
flatten(valid(invalid(['error']))) // => Invalid(['error'])
flatten(invalid(['outer error'])) // => Invalid(['outer error'])
// Data-last (in pipe)
pipe(
valid(valid(5)),
flatten()
) // => Valid(5)
// Real-world: Flatten result of mapping with validator
const validateEmail = (s: string) =>
s.includes('@') ? valid(s) : invalid('Invalid email')
pipe(
valid('user@example.com'),
map(validateEmail), // => Valid(Valid('user@example.com'))
flatten() // => Valid('user@example.com')
)
// Note: Usually you'd use flatMap instead
pipe(
valid('user@example.com'),
flatMap(validateEmail) // => Valid('user@example.com')
)See also flatMap.
fromPredicate
Creates a validator from a predicate function.
If the predicate returns true, returns Valid with the value. If the predicate returns false, returns Invalid with the error.
function fromPredicate<T, E>(predicate: Predicate<T>, error: E): Validator<T, T, E>import { gte } from '@ontologyio/receta/predicate'
// Simple predicate validator
const isPositive = fromPredicate(
(n: number) => n > 0,
'Must be positive'
)
isPositive(5) // => Valid(5)
isPositive(-1) // => Invalid(['Must be positive'])
// Using predicate builders
const isAdult = fromPredicate(
gte(18),
'Must be 18 or older'
)
// Real-world: Email validation
const isEmail = fromPredicate(
(s: string) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(s),
'Invalid email format'
)See also fromPredicateWithError.
fromPredicateWithError
Creates a validator from a predicate with a dynamic error function.
Like fromPredicate, but allows the error message to depend on the input value.
function fromPredicateWithError<T, E>(predicate: Predicate<T>, getError: (value: T) => E): Validator<T, T, E>// Dynamic error message
const minLength = (min: number) =>
fromPredicateWithError(
(s: string) => s.length >= min,
(s) => `Expected ${min} characters, got ${s.length}`
)
minLength(5)('hi')
// => Invalid(['Expected 5 characters, got 2'])
// Real-world: Range validation with context
const inRange = (min: number, max: number) =>
fromPredicateWithError(
(n: number) => n >= min && n <= max,
(n) => `${n} is out of range [${min}, ${max}]`
)See also fromPredicate.
fromResult
Converts a Result to a Validation.
Ok becomes Valid, Err becomes Invalid with a single error.
function fromResult<T, E>(result: Result<T, E>): Validation<T, E>import { ok, err, tryCatch as tryCatchResult } from '@ontologyio/receta/result'
fromResult(ok(42))
// => Valid(42)
fromResult(err('failed'))
// => Invalid(['failed'])
// Real-world: Convert Result-based parsers to Validation
const parseJSON = <T>(str: string): Validation<T, string> =>
fromResult(tryCatchResult(() => JSON.parse(str) as T, String))See also toResult.
integer
Validates number is an integer.
function integer(errorMessage?: string | undefined): Validator<number, number, string>integer()(42) // => Valid(42)
integer()(42.5) // => Invalid(['Must be an integer'])invalid
Creates an Invalid validation containing one or more errors.
Accepts either a single error or an array of errors for convenience. All errors are stored as an array internally for accumulation.
function invalid<E>(errors: E | readonly E[]): Invalid<E>// Single error
invalid('Name is required')
// => { _tag: 'Invalid', errors: ['Name is required'] }
// Multiple errors
invalid(['Name is required', 'Email is invalid'])
// => { _tag: 'Invalid', errors: ['Name is required', 'Email is invalid'] }
// Real-world: Form validation
const validatePassword = (password: string) =>
password.length < 8
? invalid(['Password too short', 'Must be at least 8 characters'])
: valid(password)isInvalid
Type guard to check if a Validation is Invalid.
Narrows the type to Invalid<E>, allowing safe access to the errors.
function isInvalid<T, E>(validation: Validation<T, E>): validation is Invalid<E>const result = invalid(['Name required', 'Email invalid'])
if (isInvalid(result)) {
console.log(result.errors) // TypeScript knows this is safe
// => ['Name required', 'Email invalid']
}
// Real-world: Error handling
const validation = validateForm(formData)
if (isInvalid(validation)) {
validation.errors.forEach(err => displayError(err))
} else {
processForm(validation.value)
}See also isValid.
isValid
Type guard to check if a Validation is Valid.
Narrows the type to Valid<T>, allowing safe access to the value.
function isValid<T, E>(validation: Validation<T, E>): validation is Valid<T>const result = valid(42)
if (isValid(result)) {
console.log(result.value) // TypeScript knows this is safe: 42
}
// Real-world: Conditional logic based on validation
const validation = validateEmail(email)
if (isValid(validation)) {
await sendEmail(validation.value)
} else {
showErrors(validation.errors)
}See also isInvalid.
lengthBetween
Validates string length within a range.
function lengthBetween(min: number, max: number, errorMessage?: string | undefined): Validator<string, string, string>lengthBetween(3, 10)('John') // => Valid('John')
lengthBetween(3, 10)('Jo') // => Invalid(['Must be between 3 and 10 characters'])
lengthBetween(3, 10)('Very long name') // => Invalid(['Must be between 3 and 10 characters'])map
Maps over the Valid value of a Validation.
If the validation is Valid, applies the function to the value. If the validation is Invalid, returns it unchanged.
function map<T, U, E>(validation: Validation<T, E>, fn: (value: T) => U): Validation<U, E>function map<T, U>(fn: (value: T) => U): <E>(validation: Validation<T, E>) => Validation<U, E>// Data-first
map(valid(5), x => x * 2) // => Valid(10)
map(invalid(['error']), x => x * 2) // => Invalid(['error'])
// Data-last (in pipe)
pipe(
valid(5),
map(x => x * 2)
) // => Valid(10)
// Real-world: Transform validated data
const validateAndFormat = (email: string) =>
pipe(
validateEmail(email),
map(e => e.toLowerCase().trim())
)See also mapInvalid, flatMap.
mapInvalid
Maps over the Invalid errors of a Validation.
If the validation is Invalid, applies the function to each error. If the validation is Valid, returns it unchanged.
function mapInvalid<T, E, F>(validation: Validation<T, E>, fn: (error: E) => F): Validation<T, F>function mapInvalid<E, F>(fn: (error: E) => F): <T>(validation: Validation<T, E>) => Validation<T, F>// Data-first
mapInvalid(invalid(['error1', 'error2']), e => e.toUpperCase())
// => Invalid(['ERROR1', 'ERROR2'])
mapInvalid(valid(42), e => e.toUpperCase())
// => Valid(42)
// Data-last (in pipe)
pipe(
invalid(['error1', 'error2']),
mapInvalid(e => e.toUpperCase())
) // => Invalid(['ERROR1', 'ERROR2'])
// Real-world: Add context to errors
const validateForm = (data: SignupForm) =>
pipe(
validateEmail(data.email),
mapInvalid(err => ({ field: 'email', message: err }))
)
// Transform error types
const toApiError = (err: string): ApiError => ({
code: 'VALIDATION_ERROR',
message: err,
timestamp: Date.now()
})
pipe(
validateInput(input),
mapInvalid(toApiError)
)See also map, flatMap.
match
Performs pattern matching on a Validation.
Exhaustively handles both Valid and Invalid cases by providing functions for each. This is the recommended way to work with Validation when you need to handle both cases.
function match<T, E, R>(validation: Validation<T, E>, matcher: ValidationMatcher<T, E, R>): Rfunction match<T, E, R>(matcher: ValidationMatcher<T, E, R>): (validation: Validation<T, E>) => R// Data-first
match(valid(42), {
onValid: (n) => `Success: ${n}`,
onInvalid: (errors) => `Errors: ${errors.join(', ')}`
})
// => "Success: 42"
match(invalid(['error1', 'error2']), {
onValid: (n) => `Success: ${n}`,
onInvalid: (errors) => `Errors: ${errors.join(', ')}`
})
// => "Errors: error1, error2"
// Data-last (in pipe)
pipe(
validateEmail(email),
match({
onValid: (email) => sendWelcomeEmail(email),
onInvalid: (errors) => showErrors(errors)
})
)
// Real-world: HTTP response
app.post('/api/users', (req, res) => {
const validation = validateUser(req.body)
match(validation, {
onValid: (user) => res.status(201).json(user),
onInvalid: (errors) => res.status(400).json({ errors })
})
})
// Real-world: UI rendering
const renderForm = (validation: Validation<SignupForm, FieldError<string>>) =>
match(validation, {
onValid: (data) => renderSuccess(data),
onInvalid: (errors) => renderErrors(errors)
})
// Real-world: Error logging
const processWithLogging = (data: unknown) =>
pipe(
validateData(data),
match({
onValid: (validated) => {
logger.info('Validation succeeded')
return handleData(validated)
},
onInvalid: (errors) => {
logger.error('Validation failed', { errors })
return null
}
})
)See also unwrapOr, unwrapOrElse.
matches
Validates string matches a regex pattern.
function matches(pattern: RegExp, errorMessage: string): Validator<string, string, string>matches(/^[A-Z]/, 'Must start with uppercase')('John') // => Valid('John')
matches(/^[A-Z]/, 'Must start with uppercase')('john') // => Invalid(['Must start with uppercase'])
// Username validation
matches(/^[a-zA-Z0-9_]+$/, 'Only letters, numbers, and underscores')max
Validates number is less than or equal to maximum.
function max(maximum: number, errorMessage?: string | undefined): Validator<number, number, string>max(100)(50) // => Valid(50)
max(100)(150) // => Invalid(['Must be at most 100'])maxItems
Validates array has maximum length.
function maxItems<T>(maxCount: number, errorMessage?: string | undefined): Validator<T[], T[], string>maxItems(5)([1, 2, 3]) // => Valid([1, 2, 3])
maxItems(2)([1, 2, 3]) // => Invalid(['Must have at most 2 items'])maxLength
Validates maximum string length.
function maxLength(max: number, errorMessage?: string | undefined): Validator<string, string, string>maxLength(10)('John') // => Valid('John')
maxLength(10)('Very long name') // => Invalid(['Must be at most 10 characters'])min
Validates number is greater than or equal to minimum.
function min(minimum: number, errorMessage?: string | undefined): Validator<number, number, string>min(0)(5) // => Valid(5)
min(0)(-1) // => Invalid(['Must be at least 0'])
min(18, 'Must be adult')(17) // => Invalid(['Must be adult'])minItems
Validates array has minimum length.
function minItems<T>(minCount: number, errorMessage?: string | undefined): Validator<T[], T[], string>minItems(2)([1, 2, 3]) // => Valid([1, 2, 3])
minItems(2)([1]) // => Invalid(['Must have at least 2 items'])minLength
Validates minimum string length.
function minLength(min: number, errorMessage?: string | undefined): Validator<string, string, string>minLength(3)('John') // => Valid('John')
minLength(3)('Jo') // => Invalid(['Must be at least 3 characters'])
minLength(3, 'Too short')('Jo') // => Invalid(['Too short'])negative
Validates number is negative (< 0).
function negative(errorMessage?: string | undefined): Validator<number, number, string>negative()(-5) // => Valid(-5)
negative()(0) // => Invalid(['Must be negative'])
negative()(1) // => Invalid(['Must be negative'])nonEmpty
Validates array is not empty.
function nonEmpty<T>(errorMessage: string): Validator<T[], T[], string>nonEmpty('List cannot be empty')([1, 2, 3]) // => Valid([1, 2, 3])
nonEmpty('List cannot be empty')([]) // => Invalid(['List cannot be empty'])notNaN
Validates number is not NaN.
function notNaN(errorMessage?: string | undefined): Validator<number, number, string>notNaN()(42) // => Valid(42)
notNaN()(NaN) // => Invalid(['Must be a valid number'])oneOf
Validates value is one of the allowed values.
function oneOf<T>(allowed: readonly T[], errorMessage?: string | undefined): Validator<T, T, string>oneOf(['admin', 'user', 'guest'])('admin') // => Valid('admin')
oneOf(['admin', 'user', 'guest'])('superadmin') // => Invalid(['Must be one of: admin, user, guest'])
// With custom error
oneOf([1, 2, 3], 'Invalid option')(2) // => Valid(2)
oneOf([1, 2, 3], 'Invalid option')(4) // => Invalid(['Invalid option'])partial
Creates a schema validator for partial objects.
Like schema, but only validates fields that are present in the value. Missing fields are not validated.
function partial<T extends object, E>(schemaObj: ValidationSchema<T, E>, value: Partial<T>): Validation<Partial<T>, FieldError<E>>function partial<T extends object, E>(schemaObj: ValidationSchema<T, E>): (value: Partial<T>) => Validation<Partial<T>, FieldError<E>>const userSchema: ValidationSchema<User, string> = {
name: (n) => n.length > 0 ? valid(n) : invalid('Name required'),
email: (e) => e.includes('@') ? valid(e) : invalid('Invalid email'),
age: (a) => a >= 18 ? valid(a) : invalid('Must be 18+')
}
// Only validates present fields
partial(userSchema, { name: 'John' })
// => Valid({ name: 'John' })
partial(userSchema, { name: '', email: 'invalid' })
// => Invalid([
// { field: 'name', errors: ['Name required'] },
// { field: 'email', errors: ['Invalid email'] }
// ])
// Real-world: PATCH request validation
app.patch('/api/users/:id', (req, res) => {
const validation = partial(userSchema, req.body)
if (isInvalid(validation)) {
return res.status(400).json({ errors: validation.errors })
}
updateUser(req.params.id, validation.value)
})See also schema.
positive
Validates number is positive (> 0).
function positive(errorMessage?: string | undefined): Validator<number, number, string>positive()(5) // => Valid(5)
positive()(0) // => Invalid(['Must be positive'])
positive()(-1) // => Invalid(['Must be positive'])required
Validates that a string is not empty.
function required(errorMessage: string): Validator<string, string, string>required('Name is required')('John') // => Valid('John')
required('Name is required')('') // => Invalid(['Name is required'])
// Whitespace only counts as empty
required('Name is required')(' ') // => Invalid(['Name is required'])schema
Validates an object against a schema, accumulating field-level errors.
Each field is validated independently using its validator from the schema. If all fields are valid, returns Valid with the object. If any field is invalid, returns Invalid with FieldError objects for each failed field.
Valid holds the object you passed in, so a validator can't convert a field or fill in a default.
function schema<T extends object, E>(schemaObj: ValidationSchema<T, E>, value: T): Validation<T, FieldError<E>>function schema<T extends object, E>(schemaObj: ValidationSchema<T, E>): (value: T) => Validation<T, FieldError<E>>type User = {
name: string
email: string
age: number
}
const userSchema: ValidationSchema<User, string> = {
name: (n) => n.length > 0 ? valid(n) : invalid('Name required'),
email: (e) => e.includes('@') ? valid(e) : invalid('Invalid email'),
age: (a) => a >= 18 ? valid(a) : invalid('Must be 18+')
}
// Valid object
schema(userSchema, { name: 'John', email: 'john@example.com', age: 25 })
// => Valid({ name: 'John', email: 'john@example.com', age: 25 })
// Invalid object - accumulates all field errors
schema(userSchema, { name: '', email: 'invalid', age: 17 })
// => Invalid([
// { field: 'name', errors: ['Name required'] },
// { field: 'email', errors: ['Invalid email'] },
// { field: 'age', errors: ['Must be 18+'] }
// ])
// Real-world: Form validation
const validateRegistration = (form: User) =>
schema(userSchema, form)
const result = validateRegistration(formData)
if (isInvalid(result)) {
// Display all field errors at once
result.errors.forEach(({ field, errors }) => {
showFieldError(field, errors)
})
}
// Real-world: API request validation
const createUserSchema: ValidationSchema<CreateUserRequest, string> = {
email: validate([required('Email required'), email()]),
password: validate([minLength(8, 'Min 8 characters'), matches(/[A-Z]/, 'Need uppercase')])
}
app.post('/api/users', (req, res) => {
const validation = schema(createUserSchema, req.body)
if (isInvalid(validation)) {
return res.status(400).json({ errors: validation.errors })
}
// Process valid user data
createUser(validation.value)
})See also validate, collectErrors.
tap
Performs a side effect with the Valid value, returning the validation unchanged.
Useful for logging, debugging, or triggering side effects in a pipeline without modifying the validation.
function tap<T, E>(validation: Validation<T, E>, fn: (value: T) => void): Validation<T, E>function tap<T>(fn: (value: T) => void): <E>(validation: Validation<T, E>) => Validation<T, E>// Data-first
tap(valid(42), (n) => console.log('Value:', n))
// Logs: "Value: 42"
// Returns: Valid(42)
tap(invalid(['error']), (n) => console.log('Value:', n))
// Logs nothing
// Returns: Invalid(['error'])
// Data-last (in pipe)
pipe(
validateEmail(email),
tap((email) => console.log('Valid email:', email)),
flatMap(sendWelcomeEmail)
)
// Real-world: Log valid values
const processUser = (data: unknown) =>
pipe(
validateUser(data),
tap((user) => logger.info('User validated', { userId: user.id })),
flatMap(saveUser)
)
// Real-world: Trigger analytics
const submitForm = (formData: FormData) =>
pipe(
validateForm(formData),
tap((data) => analytics.track('form_validated', data)),
flatMap(sendToAPI)
)
// Real-world: Debug pipeline
const debug = <T>(label: string) => (value: T) => {
console.log(label, value)
return value
}
pipe(
validateInput(input),
tap(debug('After validation')),
map(transform),
tap(debug('After transform'))
)See also tapInvalid.
tapInvalid
Performs a side effect with the Invalid errors, returning the validation unchanged.
Useful for logging errors, sending error reports, or triggering error handling side effects without modifying the validation.
function tapInvalid<T, E>(validation: Validation<T, E>, fn: (errors: readonly E[]) => void): Validation<T, E>function tapInvalid<E>(fn: (errors: readonly E[]) => void): <T>(validation: Validation<T, E>) => Validation<T, E>// Data-first
tapInvalid(invalid(['error1', 'error2']), (errors) => console.error(errors))
// Logs: ['error1', 'error2']
// Returns: Invalid(['error1', 'error2'])
tapInvalid(valid(42), (errors) => console.error(errors))
// Logs nothing
// Returns: Valid(42)
// Data-last (in pipe)
pipe(
validateForm(formData),
tapInvalid((errors) => logger.error('Validation failed', { errors })),
match({
onValid: processForm,
onInvalid: () => showErrorPage()
})
)
// Real-world: Error reporting
const processWithErrorReporting = (data: unknown) =>
pipe(
validateData(data),
tapInvalid((errors) => {
errorTracker.report('validation_error', {
errors,
context: { data }
})
}),
flatMap(handleData)
)
// Real-world: User feedback
const submitForm = (formData: FormData) =>
pipe(
validateForm(formData),
tapInvalid((errors) => {
showToast({
type: 'error',
message: `Please fix ${errors.length} error(s)`
})
}),
flatMap(sendToAPI)
)
// Real-world: Combined logging
const processWithLogging = (data: unknown) =>
pipe(
validateData(data),
tap((valid) => logger.info('Validation passed', { data: valid })),
tapInvalid((errors) => logger.error('Validation failed', { errors })),
flatMap(handleData)
)See also tap.
toResult
Converts a Validation to a Result.
Valid becomes Ok, Invalid becomes Err with the array of errors.
⚠️ Note: This loses the error accumulation capability of Validation. Result only holds a single error value (which will be the array of all errors).
function toResult<T, E>(validation: Validation<T, E>): Result<T, readonly E[]>function toResult<T, E>(): (validation: Validation<T, E>) => Result<T, readonly E[]>import { mapErr, flatMap as flatMapResult, unwrapOr as unwrapOrResult } from '@ontologyio/receta/result'
// Data-first
toResult(valid(42))
// => Ok(42)
toResult(invalid(['error1', 'error2']))
// => Err(['error1', 'error2'])
// Data-last (in pipe)
pipe(
validateEmail(email),
toResult()
)
// Real-world: Convert to Result for API that expects Result
const validateForAPI = (data: unknown) =>
pipe(
validateData(data),
toResult(),
mapErr((errors) => ({
code: 'VALIDATION_ERROR',
messages: errors
}))
)
// Real-world: Use Result's error handling
const handleInput = (input: unknown) =>
pipe(
validateInput(input),
toResult(),
flatMapResult(processValidData),
unwrapOrResult(defaultValue)
)See also fromResult.
toResultWith
Converts a Validation to a Result, combining all errors into a single error.
Like toResult, but uses a function to combine multiple errors into a single error. This is useful when you need a Result with a single error value instead of an array.
function toResultWith<T, E, F>(validation: Validation<T, E>, combineErrors: (errors: readonly E[]) => F): Result<T, F>function toResultWith<E, F>(combineErrors: (errors: readonly E[]) => F): <T>(validation: Validation<T, E>) => Result<T, F>// Combine errors into a message
toResultWith(
invalid(['Error 1', 'Error 2', 'Error 3']),
(errors) => errors.join('; ')
)
// => Err('Error 1; Error 2; Error 3')
// Use first error only
toResultWith(
invalid(['Error 1', 'Error 2']),
(errors) => errors[0] ?? 'Unknown error'
)
// => Err('Error 1')
// Real-world: Create structured error
const toApiResult = (validation: Validation<User, string>) =>
toResultWith(validation, (errors) => ({
status: 400,
code: 'VALIDATION_ERROR',
message: 'Validation failed',
details: errors
}))
// Real-world: Format for display
const toDisplayResult = (validation: Validation<Data, FieldError<string>>) =>
toResultWith(validation, (errors) => ({
title: 'Please fix the following errors:',
items: errors.map(e => `${e.field}: ${e.errors.join(', ')}`)
}))See also toResult, fromResult.
tryCatch
Wraps a potentially throwing function in a Validation.
If the function succeeds, returns Valid with the return value. If the function throws, returns Invalid with the caught error. Wraps a potentially throwing function in a Validation with error mapping.
function tryCatch<T>(fn: () => T): Validation<T, unknown>function tryCatch<T, E>(fn: () => T, mapError: (error: unknown) => E): Validation<T, E>// Parse JSON safely
const parseJSON = <T>(str: string): Validation<T, unknown> =>
tryCatch(() => JSON.parse(str) as T)
parseJSON('{"name":"John"}')
// => Valid({ name: 'John' })
parseJSON('invalid json')
// Invalid, holding the SyntaxError that JSON.parse threw// Parse JSON with custom error
const parseJSON = <T>(str: string): Validation<T, string> =>
tryCatch(
() => JSON.parse(str) as T,
(e) => `JSON parse error: ${(e as Error).message}`
)
// Real-world: Parse with validation
const parseAndValidate = <T>(str: string, validate: Validator<T, T, string>) =>
pipe(
tryCatch(() => JSON.parse(str) as T, String),
flatMap(validate)
)tryCatchAsync
Async version of tryCatch.
Wraps a potentially rejecting Promise in a Validation. Async version of tryCatch with error mapping.
function tryCatchAsync<T>(fn: () => Promise<T>): Promise<Validation<T, unknown>>function tryCatchAsync<T, E>(fn: () => Promise<T>, mapError: (error: unknown) => E): Promise<Validation<T, E>>// Fetch with validation
const fetchUser = async (id: string): Promise<Validation<User, unknown>> =>
tryCatchAsync(() => fetch(`/api/users/${id}`).then(r => r.json()))
// With error mapping
const fetchUser = async (id: string): Promise<Validation<User, string>> =>
tryCatchAsync(
() => fetch(`/api/users/${id}`).then(r => r.json()),
(e) => `Failed to fetch user: ${e}`
)// Real-world: API call with validation
const createUser = async (data: unknown): Promise<Validation<User, string>> =>
pipe(
await tryCatchAsync(
() => fetch('/api/users', {
method: 'POST',
body: JSON.stringify(data)
}).then(r => r.json()),
(e) => `Network error: ${e}`
),
flatMap(validateUserData)
)unwrap
Extracts the value from a Valid validation or throws an error.
⚠️ Warning: This function throws if the validation is Invalid. Only use when you're certain the validation is Valid, or use unwrapOr/unwrapOrElse instead.
function unwrap<T, E>(validation: Validation<T, E>): Tunwrap(valid(42)) // => 42
unwrap(invalid(['error']))
// Throws Error: Cannot unwrap Invalid validation. Errors: ["error"]
// Real-world: When you've already checked the validation
const validation = validateConfig(config)
if (isValid(validation)) {
const value = unwrap(validation) // Safe because we checked
useConfig(value)
}See also unwrapOr, unwrapOrElse, match.
unwrapOr
Extracts the value from a Valid validation or returns a default value.
function unwrapOr<T, E>(validation: Validation<T, E>, defaultValue: T): Tfunction unwrapOr<T>(defaultValue: T): <E>(validation: Validation<T, E>) => T// Data-first
unwrapOr(valid(42), 0) // => 42
unwrapOr(invalid(['error']), 0) // => 0
// Data-last (in pipe)
pipe(
validateAge(input),
unwrapOr(18)
)
// Real-world: Configuration with defaults
const getPort = (config: Config) =>
pipe(
validatePort(config.port),
unwrapOr(3000)
)
// Form field with default
const getUsername = (form: SignupForm) =>
pipe(
validateUsername(form.username),
unwrapOr('anonymous')
)See also unwrapOrElse, unwrap.
unwrapOrElse
Extracts the value from a Valid validation or computes a default from errors.
function unwrapOrElse<T, E>(validation: Validation<T, E>, fn: (errors: readonly E[]) => T): Tfunction unwrapOrElse<T, E>(fn: (errors: readonly E[]) => T): (validation: Validation<T, E>) => T// Data-first
unwrapOrElse(valid(42), () => 0)
// => 42
unwrapOrElse(invalid(['error1', 'error2']), (errors) => {
console.error('Validation failed:', errors)
return 0
})
// Logs errors and returns 0
// Data-last (in pipe)
pipe(
validateInput(input),
unwrapOrElse((errors) => {
reportErrors(errors)
return fallbackValue
})
)
// Real-world: Log errors and return default
const getConfig = (raw: unknown) =>
pipe(
validateConfig(raw),
unwrapOrElse((errors) => {
logger.error('Config validation failed', { errors })
return defaultConfig
})
)
// Real-world: Create error report
const processForm = (data: SignupForm) =>
pipe(
validateForm(data),
unwrapOrElse((errors) => {
showNotification({
type: 'error',
message: `Validation failed with ${errors.length} errors`
})
return emptyForm
})
)See also unwrapOr, match.
url
Validates URL format.
function url(errorMessage?: string | undefined): Validator<string, string, string>url()('https://example.com') // => Valid('https://example.com')
url()('not-a-url') // => Invalid(['Invalid URL'])valid
Creates a Valid validation containing a value.
function valid<T>(value: T): Valid<T>const result = valid(42)
// => { _tag: 'Valid', value: 42 }
const user = valid({ name: 'John', email: 'john@example.com' })
// => { _tag: 'Valid', value: { name: 'John', email: 'john@example.com' } }validate
Applies multiple validators to a single value, accumulating all errors.
All validators are run against the value. If any fail, all errors are accumulated. If all succeed, returns Valid with the original value.
This is useful when a single value must satisfy multiple independent constraints.
function validate<T, E>(value: T, validators: readonly Validator<T, T, E>[]): Validation<T, E>function validate<T, E>(validators: readonly Validator<T, T, E>[]): (value: T) => Validation<T, E>// Define validators
const rules = [minLength(5), maxLength(10), matches(/^\S+$/, 'No spaces allowed')]
// Apply all validators
validate('hi', rules)
// => Invalid(['Must be at least 5 characters'])
validate('hello world', rules)
// => Invalid(['Must be at most 10 characters', 'No spaces allowed'])
validate('hello', rules)
// => Valid('hello')
// Data-last: returns a validator, ready for a schema field
const validateName = validate(rules)
validateName('hello')
// => Valid('hello')
// Real-world: Password validation with multiple rules
const validatePassword = (password: string) =>
validate(password, [
fromPredicate(s => s.length >= 8, 'At least 8 characters'),
fromPredicate(s => /[A-Z]/.test(s), 'At least one uppercase'),
fromPredicate(s => /[0-9]/.test(s), 'At least one number'),
fromPredicate(s => /[^A-Za-z0-9]/.test(s), 'At least one special char')
])
validatePassword('weak')
// => Invalid([
// 'At least 8 characters',
// 'At least one uppercase',
// 'At least one number',
// 'At least one special char'
// ])
// Real-world: Email validation with multiple checks
const validateEmail = (email: string) =>
validate(email, [
fromPredicate(s => s.length > 0, 'Email required'),
fromPredicate(s => s.includes('@'), 'Must be an email address'),
fromPredicate(s => s.length <= 254, 'Email too long'),
fromPredicate(s => !/\s/.test(s), 'No whitespace allowed')
])See also collectErrors, validateAll.
validateAll
Applies a validator to each element in an array, accumulating all errors.
If all elements are valid, returns Valid with the array of values. If any element is invalid, returns Invalid with all accumulated errors.
This is useful for bulk validation operations like CSV imports or batch API requests.
function validateAll<T, U, E>(values: readonly T[], validator: Validator<T, U, E>): Validation<U[], E>function validateAll<T, U, E>(validator: Validator<T, U, E>): (values: readonly T[]) => Validation<U[], E>// Validate array of numbers
const isPositive: Validator<number, number, string> =
(n) => n > 0 ? valid(n) : invalid(`${n} is not positive`)
validateAll([1, 2, 3], isPositive)
// => Valid([1, 2, 3])
validateAll([1, -2, 3, -4], isPositive)
// => Invalid(['-2 is not positive', '-4 is not positive'])
// Real-world: Validate CSV import rows. Errors come back as one flat
// list, so each message has to name its row.
type Row = { id: string; price: number }
const validateRow = (row: Row) =>
row.price > 0 ? valid(row) : invalid(`${row.id}: price must be positive`)
validateAll([{ id: 'a', price: 5 }, { id: 'b', price: -1 }], validateRow)
// => Invalid(['b: price must be positive'])
// Real-world: Validate batch API request
const validateBatchCreate = (requests: Row[]) =>
validateAll(requests, validateRow)See also validate, collectErrors.
Types
FieldError
Represents a validation error for a specific field.
Used when validating objects to track which field(s) failed validation.
interface FieldError<E> {
readonly field: string
readonly errors: readonly E[]
}const fieldError: FieldError<string> = {
field: 'email',
errors: ['Invalid email format', 'Email already exists']
}Invalid
Represents a failed validation containing accumulated errors.
Unlike Result's Err which contains a single error, Invalid contains an array of errors to support error accumulation across multiple validations.
interface Invalid<E> {
readonly _tag: 'Invalid'
readonly errors: readonly E[]
}Valid
Represents a successful validation containing a value.
interface Valid<T> {
readonly _tag: 'Valid'
readonly value: T
}Validation
Validation type representing either success (Valid) or failure (Invalid).
Validation is similar to Result but optimized for error accumulation. While Result short-circuits on the first error (fail-fast), Validation accumulates all errors (fail-slow), making it ideal for form validation and multi-field data validation.
type Validation<T, E> = Valid<T> | Invalid<E>type FormValidation = Validation<User, string>
const success: FormValidation = { _tag: 'Valid', value: { name: 'John', email: 'john@example.com' } }
const failure: FormValidation = { _tag: 'Invalid', errors: ['Name too short', 'Invalid email'] }ValidationMatcher
Pattern matching interface for Validation.
interface ValidationMatcher<T, E, R> {
readonly onValid: (value: T) => R
readonly onInvalid: (errors: readonly E[]) => R
}ValidationSchema
A schema definition for object validation.
Maps object keys to validators that test the corresponding values.
Used with the schema combinator for validating entire objects.
type ValidationSchema<T, E> = {
[K in keyof T]: Validator<T[K], T[K], E>
}interface User {
name: string
email: string
age: number
}
const userSchema: ValidationSchema<User, string> = {
name: (n) => n.length > 0 ? valid(n) : invalid('Name required'),
email: (e) => e.includes('@') ? valid(e) : invalid('Invalid email'),
age: (a) => a >= 18 ? valid(a) : invalid('Must be 18+')
}Validator
A function that validates a value and returns a Validation.
Validators can be composed using flatMap, validate, and other combinators to build complex validation logic from simple validators.
type Validator<T, U, E> = (value: T) => Validation<U, E>const minLength = (min: number): Validator<string, string, string> =>
(value) => value.length >= min
? valid(value)
: invalid(`Must be at least ${min} characters`)