Skip to content

Validation ​

Validation<E, T> represents a computation that can succeed with a value (Valid) or fail with accumulated errors (Invalid).

  • Valid<E, T> - success, carries a value of type T
  • Invalid<E, T> - failure, carries one or more errors of type E

Either vs Validation: When to Use Which ​

Both Either and Validation model computations that can fail, but they behave very differently when you combine multiple failing computations. Choosing the right one matters.

Use Either when ​

  • Steps are dependent - each step relies on the result of the previous one. If step 1 fails, step 2 cannot proceed.
  • You want to stop at the first error - a pipeline where the first failure is the only one that matters.
  • You are sequencing operations - chaining flatMap through a series of transformations.
ts
import { right, left } from "ok-fp/either";

const parseAge = (input: string) => {
  const n = Number(input);
  return Number.isInteger(n) ? right(n) : left("Must be a whole number");
};

const validateAge = (n: number) =>
  n >= 0 && n <= 150 ? right(n) : left("Must be between 0 and 150");

// Each step depends on the previous - Either is the right choice
parseAge("abc")
  .flatMap(validateAge)
  .match(
    (err) => console.error(err), // "Must be a whole number"
    (age) => console.log(age),
  );

Use Validation when ​

  • Steps are independent - you are validating several fields or inputs that do not depend on each other.
  • You want all errors at once - reporting every problem in a form, a config file, or a batch of inputs in a single pass.
ts
import { valid, invalid, map2 } from "ok-fp/validation";

const validateName = (name: string) =>
  name.length > 0 ? valid(name) : invalid("Name is required");

const validateAge = (age: number) =>
  age >= 0 ? valid(age) : invalid("Age must be non-negative");

// Both validations are independent - Validation accumulates all errors
map2(validateName(""), validateAge(-1), (name, age) => ({ name, age })).match(
  (errors) => console.error(errors), // ["Name is required", "Age must be non-negative"]
  (user) => console.log(user),
);

Quick reference ​

ScenarioUse
Steps depend on each otherEither
Stop at the first failureEither
Validate independent fieldsValidation
Collect all errors in one passValidation
Parsing a pipeline of transformationsEither
Form or config validationValidation

The key difference in action ​

ts
import * as Either from "ok-fp/either";
import * as Validation from "ok-fp/validation";

// Either: stops at the first error
Either.map2(
  Either.left("Name is required"),
  Either.left("Age must be positive"),
  (name, age) => ({ name, age }),
);
// Left("Name is required")  ← second error is never seen

// Validation: collects all errors
Validation.map2(
  Validation.invalid("Name is required"),
  Validation.invalid("Age must be positive"),
  (name, age) => ({ name, age }),
);
// invalid(["Name is required", "Age must be positive"])  ← both errors reported

Basic Usage ​

Creating a Validation ​

ts
import { valid, invalid, fromEither, fromOption } from "ok-fp/validation";

const success = valid(42); // valid(42)
const failure = invalid("Something went wrong"); // invalid(["Something went wrong"])
const fromRight = fromEither(right(42)); // valid(42)
const fromLeft = fromEither(left("error")); // invalid(["error"])
const fromSome = fromOption(some(42), () => "missing"); // valid(42)
const fromNone = fromOption(none(), () => "missing"); // invalid(["missing"])

Combining Independent Validations ​

ts
import { valid, invalid, map2, map3 } from "ok-fp/validation";

const validateName = (name: string) =>
  name.trim().length > 0 ? valid(name.trim()) : invalid("Name is required");

const validateEmail = (email: string) =>
  email.includes("@") ? valid(email) : invalid("Invalid email address");

const validateAge = (age: number) =>
  age >= 18 ? valid(age) : invalid("Must be at least 18");

// All three are validated independently - all errors are collected
map3(
  validateName(""),
  validateEmail("not-an-email"),
  validateAge(16),
  (name, email, age) => ({ name, email, age }),
).match(
  (errors) => console.error("Errors:", errors),
  // ["Name is required", "Invalid email address", "Must be at least 18"]
  (user) => console.log("User:", user),
);

Extracting the Value ​

ts
import { valid, invalid } from "ok-fp/validation";

const x = valid(42);
x.getOrElse(() => 0); // 42

const y = invalid("error");
y.getOrElse(() => 0); // 0

// Pattern matching
const result = x.match(
  (errors) => `Errors: ${errors.join(", ")}`, // Invalid case
  (val) => `Value: ${val}`, // Valid case
);
// result = "Value: 42"

API Reference ​

valid ​

ts
valid<T, E = never>(value: T): Validation<E, T>

Create a Validation containing a success value.

ts
valid(42); // valid(42)
valid("hello"); // valid("hello")

invalid ​

ts
invalid<E, T = never>(error: E): Validation<E, T>

Create a Validation containing a single error. The error is wrapped in an array internally.

ts
invalid("Something went wrong"); // invalid(["Something went wrong"])
invalid(404); // invalid([404])

fromEither ​

ts
fromEither<E, T>(either: Either<E, T>): Validation<E, T>

Convert an Either to a Validation. Right becomes Valid, Left becomes Invalid with a single error.

ts
fromEither(right(42)); // valid(42)
fromEither(left("error")); // invalid(["error"])

fromOption ​

ts
fromOption<E, T>(opt: Option<T>, onNone: () => E): Validation<E, T>

Convert an Option to a Validation. Some becomes Valid, None becomes Invalid with the provided error.

ts
fromOption(some(42), () => "missing"); // valid(42)
fromOption(none(), () => "missing"); // invalid(["missing"])

map ​

ts
map<U>(mapper: (value: T) => U): Validation<E, U>

Transform the Valid value. Invalid is passed through unchanged.

ts
valid(5).map((n) => n * 2); // valid(10)
invalid("error").map((n) => n * 2); // invalid(["error"])

filterOrElse ​

ts
filterOrElse(predicate: (value: T) => boolean, onInvalid: () => E): Validation<E, T>

Keep the Valid value only if the predicate holds. Returns Invalid with the provided error otherwise.

ts
valid(5).filterOrElse(
  (n) => n > 3,
  () => "too small",
); // valid(5)
valid(2).filterOrElse(
  (n) => n > 3,
  () => "too small",
); // invalid(["too small"])
invalid("error").filterOrElse(
  (n) => n > 3,
  () => "too small",
); // invalid(["error"])

ap ​

ts
ap<EE, A, U>(this: Validation<E, (a: A) => U>, arg: Validation<EE, A>): Validation<E | EE, U>

Apply a function wrapped in a Validation to a value wrapped in a Validation. Unlike Either.ap, if both are Invalid errors are accumulated.

ts
const add = (x: number) => (y: number) => x + y;
valid(add(5)).ap(valid(3)); // valid(8)
valid(add(5)).ap(invalid("err")); // invalid(["err"])
invalid("e1").ap(invalid("e2")); // invalid(["e1", "e2"]) ← errors accumulated!

zip ​

ts
zip<EE, A>(valA: Validation<EE, A>): Validation<E | EE, readonly [T, A]>

Combine two Validations into a tuple. If both are Valid, returns Valid with a tuple. If either is Invalid, accumulates all errors.

ts
valid("Alice").zip(valid(30)); // valid(["Alice", 30])
valid("Alice").zip(invalid("No age")); // invalid(["No age"])
invalid("e1").zip(invalid("e2")); // invalid(["e1", "e2"])

tap ​

ts
tap(sideEffect: (value: T) => void): Validation<E, T>

Run a side effect if Valid. Returns the original Validation unchanged.

ts
valid(42).tap((v) => console.log("value:", v)); // valid(42), logs "value: 42"
invalid("err").tap((v) => console.log("value:", v)); // invalid(["err"]), no log

tapInvalid ​

ts
tapInvalid(sideEffect: (errors: readonly E[]) => void): Validation<E, T>

Run a side effect if Invalid. Returns the original Validation unchanged.

ts
valid(42).tapInvalid((errs) => console.log(errs)); // valid(42), no log
invalid("err").tapInvalid((errs) => console.log(errs)); // invalid(["err"]), logs ["err"]

match ​

ts
match<U>(onInvalid: (errors: readonly E[]) => U, onValid: (value: T) => U): U

Pattern match on the Validation.

ts
valid(42).match(
  (errors) => `Errors: ${errors.join(", ")}`,
  (val) => `Value: ${val}`,
); // "Value: 42"

invalid("oops").match(
  (errors) => `Errors: ${errors.join(", ")}`,
  (val) => `Value: ${val}`,
); // "Errors: oops"

getOrElse ​

ts
getOrElse(fallback: (errors: readonly E[]) => T): T

Extract the Valid value, or return a fallback computed from the errors.

ts
valid(42).getOrElse(() => 0); // 42
invalid("error").getOrElse(() => 0); // 0

toResult ​

ts
toResult(): ValidationResult<E, T>

Convert Validation to a plain ValidationResult object: { ok: true, value: T } or { ok: false, errors: readonly E[] }.

ts
valid(42).toResult(); // { ok: true, value: 42 }
invalid("error").toResult(); // { ok: false, errors: ["error"] }

map2 ​

ts
map2<EA, A, EB, B, C>(valA: Validation<EA, A>, valB: Validation<EB, B>, mapper: (a: A, b: B) => C): Validation<EA | EB, C>

Combine two Validations with a function. Accumulates all errors from both if either is Invalid.

ts
map2(valid("John"), valid("Doe"), (f, l) => `${f} ${l}`); // valid("John Doe")
map2(invalid("e1"), invalid("e2"), (f, l) => `${f} ${l}`); // invalid(["e1", "e2"])

map3 ​

ts
map3<EA, A, EB, B, EC, C, D>(valA: Validation<EA, A>, valB: Validation<EB, B>, valC: Validation<EC, C>, mapper: (a: A, b: B, c: C) => D): Validation<EA | EB | EC, D>

Combine three Validations with a function. Accumulates all errors from all three if any are Invalid.

ts
map3(valid(1), valid(2), valid(3), (a, b, c) => a + b + c); // valid(6)
map3(invalid("e1"), valid(2), invalid("e3"), (a, b, c) => a + b + c); // invalid(["e1", "e3"])

sequence ​

ts
sequence<E, T>(validations: readonly Validation<E, T>[]): Validation<E, T[]>

Convert an array of Validations to a Validation of array. Returns Valid with all values if all are Valid, otherwise accumulates all errors.

ts
sequence([valid(1), valid(2), valid(3)]); // valid([1, 2, 3])
sequence([valid(1), invalid("e1"), invalid("e2")]); // invalid(["e1", "e2"])