Return typed results instead of throwing errors

TypeScript
// Result<T, E> makes failure explicit and keeps callers honest.
type Result<T, E> =
  | { readonly ok: true; readonly value: T }
  | { readonly ok: false; readonly error: E };

const ok = <T,>(value: T): Result<T, never> => ({ ok: true, value });
const err = <E>(error: E): Result<never, E> => ({ ok: false, error });

function parsePort(input: string): Result<number, string> {
  const port = Number(input);
  if (!Number.isInteger(port) || port < 1 || port > 65535) {
    return err(`"${input}" is not a valid port`);
  }
  return ok(port);
}

function mapResult<T, U, E>(source: Result<T, E>, fn: (value: T) => U): Result<U, E> {
  return source.ok ? ok(fn(source.value)) : source;
}

function unwrapOr<T, E>(source: Result<T, E>, fallback: T): T {
  return source.ok ? source.value : fallback;
}

const doubled = mapResult(parsePort('8080'), (port) => port * 2);
console.log(unwrapOr(doubled, 0)); // 16160