Skip to content

These — inclusive OR

Most operations produce one of two outcomes: a value or a failure. These<E, A> is for the cases that don’t fit that model — when an operation partially succeeds, or succeeds but has something worth noting alongside the result. Where Result<E, A> is either an error or a value, These<E, A> has three variants:

  • Err(e) — only an error, no value
  • Ok(a) — only a value, no error
  • Both(e, a) — an error and a value simultaneously

The Both case is what makes These distinct. It represents partial success: the operation produced a result, but there’s also something worth noting — a warning, a degradation, a recovered fallback.

Result is binary: you either succeed or fail. But some operations produce a meaningful result and a diagnostic at the same time:

  • Parsing a number from a string with extra whitespace: the number is valid, but the input was malformed
  • A migration that completed with some rows skipped
  • An API response that returned data but also included deprecation warnings
  • A computation that succeeded using a fallback value when the primary source was unavailable

In these cases, returning Err discards the result. Returning Ok discards the warning. Both holds both.

import { These } from "@nlozgachev/pipekit/Core";

These.ok(42);              // Ok — success, no warning
These.err("bad input");    // Err — failure, no value
These.both("trimmed", 42); // Both — value with a warning attached

A typical use: a parser that’s lenient but reports what it fixed:

import { pipe } from "@nlozgachev/pipekit/Composition";

const parseNumber = (s: string): These<string, number> => {
  const trimmed = s.trim();
  const n = parseFloat(trimmed);
  if (isNaN(n)) return These.err("Not a number");
  if (s !== trimmed) return These.both("Leading/trailing whitespace trimmed", n);
  return These.ok(n);
};

parseNumber("  42  "); // Both("Leading/trailing whitespace trimmed", 42)
parseNumber("42");     // Ok(42)
parseNumber("abc");    // Err("Not a number")

map transforms the value in Ok and Both, leaving Err untouched. In Both, the warning is preserved:

pipe(These.ok(5),          These.map((n) => n * 2)); // Ok(10)
pipe(These.both("warn", 5), These.map((n) => n * 2)); // Both("warn", 10)
pipe(These.err("err"),      These.map((n) => n * 2)); // Err("err")

mapErr transforms the error/warning in Err and Both, leaving Ok untouched:

pipe(These.err("warn"),      These.mapErr((e) => e.toUpperCase())); // Err("WARN")
pipe(These.both("warn", 5),  These.mapErr((e) => e.toUpperCase())); // Both("WARN", 5)

bimap transforms both sides at once:

pipe(
  These.both("warn", 5),
  These.bimap(
    (e) => e.toUpperCase(),
    (n) => n * 2,
  ),
); // Both("WARN", 10)

chain passes the value to the next step. The key behaviour is in the Both case: if the current state is Both(e, a) and the next step returns Ok(b), the warning e is preserved in a Both(e, b). The warning travels forward:

const double = (n: number): These<string, number> => These.ok(n * 2);

pipe(These.ok(5),           These.chain(double)); // Ok(10)
pipe(These.both("warn", 5), These.chain(double)); // Both("warn", 10) — warning preserved
pipe(These.err("err"),      These.chain(double)); // Err("err")

If the next step itself returns Both or Err, that result takes precedence and the original warning from Both is dropped.

match — handle all three cases. Required to cover all variants:

pipe(
  result,
  These.match({
    ok:   (value)         => `Success: ${value}`,
    err:  (error)         => `Failed: ${error}`,
    both: (error, value)  => `Success with warning — ${error}: ${value}`,
  }),
);

fold — same with positional arguments:

pipe(
  result,
  These.fold(
    (error)        => `Failed: ${error}`,
    (value)        => `Success: ${value}`,
    (error, value) => `Partial: ${error}: ${value}`,
  ),
);

getOrElse — returns the value from Ok or Both, or a fallback for Err:

pipe(These.ok(5),           These.getOrElse(0)); // 5
pipe(These.both("warn", 5), These.getOrElse(0)); // 5
pipe(These.err("err"),      These.getOrElse(0)); // 0

For checking the variant directly:

These.isOk(t);    // true if Ok only
These.isErr(t);   // true if Err only
These.isBoth(t);  // true if Both

These.hasValue(t); // true if Ok or Both — value is present
These.hasError(t); // true if Err or Both — error is present

hasValue and hasError are useful when you care about the presence of each side independently, without needing to distinguish the exact variant.

toResult — converts to Result, discarding any warning from Both:

These.toResult(These.ok(42));           // Ok(42)
These.toResult(These.both("warn", 42)); // Ok(42) — warning dropped
These.toResult(These.err("err"));       // Err("err")

toOption — keeps only the value side:

These.toOption(These.ok(42));           // Some(42)
These.toOption(These.both("warn", 42)); // Some(42)
These.toOption(These.err("err"));       // None

swap — flips error and value roles:

These.swap(These.err("err"));        // Ok("err")
These.swap(These.ok(5));             // Err(5)
These.swap(These.both("warn", 5));   // Both(5, "warn")

Use These when:

  • An operation can succeed and produce a diagnostic simultaneously
  • You need to propagate warnings through a pipeline without losing the result
  • You’re building lenient parsers or processors that collect notices alongside output

Use Result when:

  • An operation either succeeds or fails — there’s no meaningful “partial” state
  • You don’t need to carry diagnostics alongside successful values

These is the less commonly reached-for type in the family. When you find yourself wanting to attach metadata, notes, or warnings to a successful result rather than just returning the result alone, that’s the signal to reach for it.