receta

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.

Signature
function alphanumeric(errorMessage?: string | undefined): Validator<string, string, string>
Example
alphanumeric()('John123') // => Valid('John123')
alphanumeric()('John@123') // => Invalid(['Must contain only letters and numbers'])

between

Validates number is within a range.

Signature
function between(minimum: number, maximum: number, errorMessage?: string | undefined): Validator<number, number, string>
Example
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.

Data-first
function collectErrors<T, E>(validations: readonly Validation<T, E>[]): Validation<T[], E>
Data-last
function collectErrors<T>(): <E>(validations: readonly Validation<T, E>[]) => Validation<T[], E>
Example
// 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 errors

See also validate, validateAll.

defined

Validates value is not null or undefined.

Signature
function defined<T>(errorMessage: string): Validator<T | null | undefined, T, string>
Example
defined('Value required')(42) // => Valid(42)
defined('Value required')(null) // => Invalid(['Value required'])
defined('Value required')(undefined) // => Invalid(['Value required'])

email

Validates email address format.

Uses a simple but effective regex pattern for email validation.

Signature
function email(errorMessage?: string | undefined): Validator<string, string, string>
Example
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.

Signature
function equals<T>(expected: T, errorMessage?: string | undefined): Validator<T, T, string>
Example
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).

Signature
function finite(errorMessage?: string | undefined): Validator<number, number, string>
Example
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.

Data-first
function flatMap<T, U, E>(validation: Validation<T, E>, fn: (value: T) => Validation<U, E>): Validation<U, E>
Data-last
function flatMap<T, U, E>(fn: (value: T) => Validation<U, E>): (validation: Validation<T, E>) => Validation<U, E>
Example
// 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.

Data-first
function flatten<T, E>(validation: Validation<Validation<T, E>, E>): Validation<T, E>
Data-last
function flatten<T, E>(): (validation: Validation<Validation<T, E>, E>) => Validation<T, E>
Example
// 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.

Signature
function fromPredicate<T, E>(predicate: Predicate<T>, error: E): Validator<T, T, E>
Example
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.

Signature
function fromPredicateWithError<T, E>(predicate: Predicate<T>, getError: (value: T) => E): Validator<T, T, E>
Example
// 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.

Signature
function fromResult<T, E>(result: Result<T, E>): Validation<T, E>
Example
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.

Signature
function integer(errorMessage?: string | undefined): Validator<number, number, string>
Example
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.

Signature
function invalid<E>(errors: E | readonly E[]): Invalid<E>
Example
// 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.

Signature
function isInvalid<T, E>(validation: Validation<T, E>): validation is Invalid<E>
Example
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.

Signature
function isValid<T, E>(validation: Validation<T, E>): validation is Valid<T>
Example
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.

Signature
function lengthBetween(min: number, max: number, errorMessage?: string | undefined): Validator<string, string, string>
Example
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.

Data-first
function map<T, U, E>(validation: Validation<T, E>, fn: (value: T) => U): Validation<U, E>
Data-last
function map<T, U>(fn: (value: T) => U): <E>(validation: Validation<T, E>) => Validation<U, E>
Example
// 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.

Data-first
function mapInvalid<T, E, F>(validation: Validation<T, E>, fn: (error: E) => F): Validation<T, F>
Data-last
function mapInvalid<E, F>(fn: (error: E) => F): <T>(validation: Validation<T, E>) => Validation<T, F>
Example
// 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.

Data-first
function match<T, E, R>(validation: Validation<T, E>, matcher: ValidationMatcher<T, E, R>): R
Data-last
function match<T, E, R>(matcher: ValidationMatcher<T, E, R>): (validation: Validation<T, E>) => R
Example
// 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.

Signature
function matches(pattern: RegExp, errorMessage: string): Validator<string, string, string>
Example
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.

Signature
function max(maximum: number, errorMessage?: string | undefined): Validator<number, number, string>
Example
max(100)(50) // => Valid(50)
max(100)(150) // => Invalid(['Must be at most 100'])

maxItems

Validates array has maximum length.

Signature
function maxItems<T>(maxCount: number, errorMessage?: string | undefined): Validator<T[], T[], string>
Example
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.

Signature
function maxLength(max: number, errorMessage?: string | undefined): Validator<string, string, string>
Example
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.

Signature
function min(minimum: number, errorMessage?: string | undefined): Validator<number, number, string>
Example
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.

Signature
function minItems<T>(minCount: number, errorMessage?: string | undefined): Validator<T[], T[], string>
Example
minItems(2)([1, 2, 3]) // => Valid([1, 2, 3])
minItems(2)([1]) // => Invalid(['Must have at least 2 items'])

minLength

Validates minimum string length.

Signature
function minLength(min: number, errorMessage?: string | undefined): Validator<string, string, string>
Example
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).

Signature
function negative(errorMessage?: string | undefined): Validator<number, number, string>
Example
negative()(-5) // => Valid(-5)
negative()(0) // => Invalid(['Must be negative'])
negative()(1) // => Invalid(['Must be negative'])

nonEmpty

Validates array is not empty.

Signature
function nonEmpty<T>(errorMessage: string): Validator<T[], T[], string>
Example
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.

Signature
function notNaN(errorMessage?: string | undefined): Validator<number, number, string>
Example
notNaN()(42) // => Valid(42)
notNaN()(NaN) // => Invalid(['Must be a valid number'])

oneOf

Validates value is one of the allowed values.

Signature
function oneOf<T>(allowed: readonly T[], errorMessage?: string | undefined): Validator<T, T, string>
Example
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.

Data-first
function partial<T extends object, E>(schemaObj: ValidationSchema<T, E>, value: Partial<T>): Validation<Partial<T>, FieldError<E>>
Data-last
function partial<T extends object, E>(schemaObj: ValidationSchema<T, E>): (value: Partial<T>) => Validation<Partial<T>, FieldError<E>>
Example
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).

Signature
function positive(errorMessage?: string | undefined): Validator<number, number, string>
Example
positive()(5) // => Valid(5)
positive()(0) // => Invalid(['Must be positive'])
positive()(-1) // => Invalid(['Must be positive'])

required

Validates that a string is not empty.

Signature
function required(errorMessage: string): Validator<string, string, string>
Example
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.

Data-first
function schema<T extends object, E>(schemaObj: ValidationSchema<T, E>, value: T): Validation<T, FieldError<E>>
Data-last
function schema<T extends object, E>(schemaObj: ValidationSchema<T, E>): (value: T) => Validation<T, FieldError<E>>
Example
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.

Data-first
function tap<T, E>(validation: Validation<T, E>, fn: (value: T) => void): Validation<T, E>
Data-last
function tap<T>(fn: (value: T) => void): <E>(validation: Validation<T, E>) => Validation<T, E>
Example
// 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.

Data-first
function tapInvalid<T, E>(validation: Validation<T, E>, fn: (errors: readonly E[]) => void): Validation<T, E>
Data-last
function tapInvalid<E>(fn: (errors: readonly E[]) => void): <T>(validation: Validation<T, E>) => Validation<T, E>
Example
// 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).

Data-first
function toResult<T, E>(validation: Validation<T, E>): Result<T, readonly E[]>
Data-last
function toResult<T, E>(): (validation: Validation<T, E>) => Result<T, readonly E[]>
Example
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.

Data-first
function toResultWith<T, E, F>(validation: Validation<T, E>, combineErrors: (errors: readonly E[]) => F): Result<T, F>
Data-last
function toResultWith<E, F>(combineErrors: (errors: readonly E[]) => F): <T>(validation: Validation<T, E>) => Result<T, F>
Example
// 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.

Overload 1
function tryCatch<T>(fn: () => T): Validation<T, unknown>
Overload 2
function tryCatch<T, E>(fn: () => T, mapError: (error: unknown) => E): Validation<T, E>
Example
// 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
Example
// 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.

Overload 1
function tryCatchAsync<T>(fn: () => Promise<T>): Promise<Validation<T, unknown>>
Overload 2
function tryCatchAsync<T, E>(fn: () => Promise<T>, mapError: (error: unknown) => E): Promise<Validation<T, E>>
Example
// 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}`
  )
Example
// 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.

Signature
function unwrap<T, E>(validation: Validation<T, E>): T
Example
unwrap(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.

Data-first
function unwrapOr<T, E>(validation: Validation<T, E>, defaultValue: T): T
Data-last
function unwrapOr<T>(defaultValue: T): <E>(validation: Validation<T, E>) => T
Example
// 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.

Data-first
function unwrapOrElse<T, E>(validation: Validation<T, E>, fn: (errors: readonly E[]) => T): T
Data-last
function unwrapOrElse<T, E>(fn: (errors: readonly E[]) => T): (validation: Validation<T, E>) => T
Example
// 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.

Signature
function url(errorMessage?: string | undefined): Validator<string, string, string>
Example
url()('https://example.com') // => Valid('https://example.com')
url()('not-a-url') // => Invalid(['Invalid URL'])

valid

Creates a Valid validation containing a value.

Signature
function valid<T>(value: T): Valid<T>
Example
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.

Data-first
function validate<T, E>(value: T, validators: readonly Validator<T, T, E>[]): Validation<T, E>
Data-last
function validate<T, E>(validators: readonly Validator<T, T, E>[]): (value: T) => Validation<T, E>
Example
// 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.

Data-first
function validateAll<T, U, E>(values: readonly T[], validator: Validator<T, U, E>): Validation<U[], E>
Data-last
function validateAll<T, U, E>(validator: Validator<T, U, E>): (values: readonly T[]) => Validation<U[], E>
Example
// 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.

Signature
interface FieldError<E> {
  readonly field: string
  readonly errors: readonly E[]
}
Example
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.

Signature
interface Invalid<E> {
  readonly _tag: 'Invalid'
  readonly errors: readonly E[]
}

Valid

Represents a successful validation containing a value.

Signature
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.

Signature
type Validation<T, E> = Valid<T> | Invalid<E>
Example
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.

Signature
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.

Signature
type ValidationSchema<T, E> = {
  [K in keyof T]: Validator<T[K], T[K], E>
}
Example
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.

Signature
type Validator<T, U, E> = (value: T) => Validation<U, E>
Example
const minLength = (min: number): Validator<string, string, string> =>
  (value) => value.length >= min
    ? valid(value)
    : invalid(`Must be at least ${min} characters`)

On this page