@onrails/result
Version:
Tagged Result / ResultAsync for railway-oriented TypeScript — pure tagged unions, neverthrow-shaped compat shim, FL-friendly
592 lines (583 loc) • 16.2 kB
JavaScript
;
// src/internal/dual.ts
var dual = (arity, body) => {
const run = body;
const dispatcher = (...args) => args.length >= arity ? run(...args) : (self) => run(self, ...args);
return dispatcher;
};
// src/result.ts
var ok = (value) => ({
_tag: "Ok",
value
});
var of = ok;
var err = (error) => ({
_tag: "Err",
error
});
var isOk = (result) => result._tag === "Ok";
var isErr = (result) => result._tag === "Err";
var map = dual(
2,
(result, fn) => isOk(result) ? ok(fn(result.value)) : err(result.error)
);
var mapErr = dual(
2,
(result, fn) => isErr(result) ? err(fn(result.error)) : ok(result.value)
);
var bimap = dual(
3,
(result, onOk, onErr) => isOk(result) ? ok(onOk(result.value)) : err(onErr(result.error))
);
var flatMap = dual(
2,
(result, fn) => isOk(result) ? fn(result.value) : err(result.error)
);
var recover = dual(
2,
(result, fn) => isErr(result) ? fn(result.error) : ok(result.value)
);
var tap = dual(2, (result, fn) => {
if (isOk(result)) fn(result.value);
return result;
});
var tapErr = dual(2, (result, fn) => {
if (isErr(result)) fn(result.error);
return result;
});
var match = dual(
3,
(result, onOk, onErr) => isOk(result) ? onOk(result.value) : onErr(result.error)
);
var unwrapOr = dual(
2,
(result, defaultValue) => isOk(result) ? result.value : defaultValue
);
function unwrapOk(result) {
if (isErr(result)) throw result.error;
return result.value;
}
var unwrap = unwrapOk;
function unwrapErr(result) {
if (isOk(result)) throw new TypeError("unwrapErr called on Ok");
return result.error;
}
function trySync(fn, onThrow) {
return (...args) => {
try {
return ok(fn(...args));
} catch (error) {
return err(onThrow(error));
}
};
}
function pipe(value, ...fns) {
let acc = value;
for (const fn of fns) {
acc = fn(acc);
}
return acc;
}
var printPayload = (payload) => {
try {
return JSON.stringify(payload) ?? String(payload);
} catch {
return String(payload);
}
};
var show = (result) => isOk(result) ? `Ok(${printPayload(result.value)})` : `Err(${printPayload(result.error)})`;
// src/types.ts
var UnexpectedError = class extends Error {
constructor(message, cause) {
super(message);
this.cause = cause;
this.name = "UnexpectedError";
}
cause;
};
// src/async.ts
var sequenceSettled = (settled) => {
const values = [];
for (const result of settled) {
if (isErr(result)) {
return err(result.error);
}
values.push(result.value);
}
return ok(values);
};
var ResultAsync = class _ResultAsync {
constructor(run) {
this.run = run;
}
run;
promise = null;
/**
* Lifts an already-settled sync {@link Result} into a {@link ResultAsync}.
*
* @example
* ```ts
* const ra = ResultAsync.fromResult(ok(42)); // ResultAsync<number, never>
* ```
*/
static fromResult(result) {
return new _ResultAsync(async () => result);
}
/**
* Wraps a `PromiseLike<T>` that may reject. Rejections pass through `onReject`
* to become a typed `Err`; resolution becomes `Ok<T>`.
*
* @param promise - the promise to wrap
* @param onReject - maps a rejection reason to the `Err` channel
*
* @example
* ```ts
* const body = ResultAsync.fromPromise(
* fetch(url).then((r) => r.text()),
* (e): NetError => ({ kind: "net", cause: String(e) }),
* );
* ```
*/
static fromPromise(promise, onReject) {
return new _ResultAsync(async () => {
try {
return ok(await promise);
} catch (error) {
return err(onReject(error));
}
});
}
/**
* Wraps a `PromiseLike<T>` that is **guaranteed not to reject**, skipping the
* `onReject` mapper. Use only when rejection is provably impossible.
*
* @example
* ```ts
* const now = ResultAsync.fromSafePromise(Promise.resolve(Date.now()));
* ```
*/
static fromSafePromise(promise) {
return new _ResultAsync(async () => ok(await promise));
}
/**
* Defers work until {@link resolve}. Unlike {@link fromPromise}, the factory
* does not run until the `ResultAsync` is resolved (e.g. by `combineTuple` /
* `combineTupleParallel`). The factory runs at most once — `resolve` memoizes.
*
* @example
* ```ts
* const insert = ResultAsync.defer(() => db.orders.insert(row));
* // nothing has run yet
* const r = await insert; // factory runs exactly once here
* ```
*/
static defer(fn) {
return new _ResultAsync(fn);
}
static ok(value) {
return new _ResultAsync(async () => ok(value));
}
static of(value) {
return _ResultAsync.ok(value);
}
/**
* Lifts an error into an `Err` async result.
*
* @example
* ```ts
* const r = ResultAsync.err({ kind: "not_found" as const });
* ```
*/
static err(error) {
return new _ResultAsync(async () => err(error));
}
/**
* Lifts an existing `Promise<Result<T, E>>` (e.g. from interop code) into a
* {@link ResultAsync}. A thrown defect is routed through `onDefect`,
* defaulting to {@link UnexpectedError}. The `Ok` value passes through
* verbatim — even when it happens to be Result-shaped.
*
* @param promise - a promise that already yields a `Result`
* @param onDefect - maps an unexpected throw to the `Err` channel
* @see {@link fromAsync} from `@onrails/result`
*/
static fromResultPromise(promise, onDefect) {
const mapDefect = onDefect ?? ((error) => new UnexpectedError("Unexpected async defect", error));
return new _ResultAsync(async () => {
try {
return await promise;
} catch (error) {
return err(mapDefect(error));
}
});
}
/**
* Combines a homogeneous array of async results into one. Resolves
* **sequentially** in input order, short-circuiting on the first `Err`.
* For heterogeneous tuples that preserve per-index types, use
* {@link combineTuple}; for wall-clock overlap, {@link combineTupleParallel}.
*
* @example
* ```ts
* const all = ResultAsync.combine([loadA(), loadB(), loadC()]);
* // ResultAsync<Item[], LoadError>
* ```
*/
static combine(results) {
return new _ResultAsync(async () => {
const values = [];
for (const ra of results) {
const result = await ra.resolve();
if (isErr(result)) {
return err(result.error);
}
values.push(result.value);
}
return ok(values);
});
}
/**
* Heterogeneous async tuple combine — resolves branches **sequentially**
* (left-to-right), returning the first `Err` in input order. Preserves each
* branch's `Ok` type by position, so the result destructures type-safely.
* This is the canonical sequential async combine (replaces the former
* `sequenceTupleAsync`).
*
* @example
* ```ts
* const combined = ResultAsync.combineTuple([loadCfg(), loadCatalog()] as const);
* // ResultAsync<readonly [Cfg, Catalog], CfgError | CatalogError>
* const r = await combined;
* if (isOk(r)) {
* const [cfg, catalog] = r.value; // typed per position
* }
* ```
*/
static combineTuple(results) {
return _ResultAsync.combine(
results
);
}
/**
* Like {@link combineTuple}, but starts every branch before awaiting (wall-clock
* parallel for independent IO). On failure, returns the first `Err` in input order.
*
* @example
* ```ts
* // independent IO — overlap them
* const combined = ResultAsync.combineTupleParallel([
* loadProfile(id),
* loadMetrics(id),
* ] as const);
* ```
*/
static combineTupleParallel(results) {
return new _ResultAsync(
async () => sequenceSettled(await Promise.all(results.map((ra) => ra.resolve())))
);
}
/**
* Transforms the `Ok` value, passing `Err` through unchanged.
*
* @example
* ```ts
* ResultAsync.ok(2).map((n) => n * 3); // ResultAsync<number> → Ok 6
* ```
*/
map(fn) {
return new _ResultAsync(async () => map(await this.resolve(), fn));
}
/**
* Transforms the `Err` value, passing `Ok` through unchanged — useful for
* unifying heterogeneous failures into one app-level union.
*
* @example
* ```ts
* load(id).mapErr((e): AppError => ({ kind: "load", cause: e })); // ResultAsync<T, AppError>
* ```
*/
mapErr(fn) {
return new _ResultAsync(async () => mapErr(await this.resolve(), fn));
}
/**
* Canonical bind — chains a step that itself returns a `ResultAsync` or sync
* `Result`, short-circuiting on `Err`. Error types accumulate (`E | F`).
*
* @example
* ```ts
* authenticate(req)
* .flatMap((user) => loadProfile(user.id)) // ResultAsync
* .flatMap((p) => validate(p)); // sync Result also accepted
* ```
*/
flatMap(fn) {
return new _ResultAsync(async () => {
const first = await this.resolve();
if (isErr(first)) {
return first;
}
const next = fn(first.value);
return next instanceof _ResultAsync ? next.resolve() : next;
});
}
/**
* neverthrow-compat alias of {@link flatMap}. Kept as the documented compat
* tier; prefer {@link flatMap} in new code.
*
* @example
* ```ts
* authenticate(req).andThen((user) => loadProfile(user.id));
* ```
*/
andThen(fn) {
return this.flatMap(fn);
}
/**
* Error-channel bind — runs `fn` only on `Err`, swapping in a recovery
* `ResultAsync` or sync `Result`. `Ok` passes through. The mirror of
* {@link flatMap} on the error track.
*
* @example
* ```ts
* loadFromCache(id).recover(() => loadFromOrigin(id)); // ResultAsync<T, F>
* ```
*/
recover(fn) {
return new _ResultAsync(async () => {
const first = await this.resolve();
if (!isErr(first)) {
return first;
}
const next = fn(first.error);
return next instanceof _ResultAsync ? next.resolve() : next;
});
}
/**
* neverthrow-compat alias of {@link recover}. Kept as the documented compat
* tier; prefer {@link recover} in new code.
*
* @example
* ```ts
* loadFromCache(id).orElse(() => loadFromOrigin(id));
* ```
*/
orElse(fn) {
return this.recover(fn);
}
/**
* Runs a side effect on the `Ok` value and passes the result through
* unchanged; a no-op on `Err`. Mirrors {@link tapErr}.
*
* @example
* ```ts
* saveUser(u).tap((saved) => analytics.track("user_saved", saved.id));
* ```
*/
tap(fn) {
return new _ResultAsync(async () => {
const result = await this.resolve();
if (!isErr(result)) {
fn(result.value);
}
return result;
});
}
/**
* Runs a side effect on the `Err` value and passes the result through
* unchanged; a no-op on `Ok`. The error-track mirror of {@link tap}.
*
* @example
* ```ts
* saveUser(u).tapErr((e) => logger.warn("save failed", e));
* ```
*/
tapErr(fn) {
return new _ResultAsync(async () => {
const result = await this.resolve();
if (isErr(result)) {
fn(result.error);
}
return result;
});
}
/**
* Settles to the `Ok` value, or `defaultValue` if `Err`. Terminal — returns a
* plain `Promise`, not a `ResultAsync`.
*
* @example
* ```ts
* const profile = await loadProfile(id).unwrapOr(guestProfile);
* ```
*/
unwrapOr(defaultValue) {
return this.resolve().then((result) => isErr(result) ? defaultValue : result.value);
}
/**
* Terminal collapse — folds both tracks into a single awaited value. Returns
* a plain `Promise`, settling the carrier exactly once.
*
* @param onOk - handles the `Ok` value
* @param onErr - handles the `Err` value
*
* @example
* ```ts
* const status = await save(row).match(
* () => 200,
* (e) => (e.kind === "conflict" ? 409 : 500),
* );
* ```
*/
match(onOk, onErr) {
return this.resolve().then(
(result) => isErr(result) ? onErr(result.error) : onOk(result.value)
);
}
/**
* Settles the carrier to a bare sync {@link Result}, memoizing so the
* underlying factory runs at most once. Prefer `await ra` (the thenable) or
* {@link match} in app code; use `resolve` when you need the tagged union back.
*
* @example
* ```ts
* const r = await load(id).resolve(); // Result<Data, LoadError>
* if (isOk(r)) use(r.value);
* ```
*/
resolve() {
if (!this.promise) {
this.promise = this.run();
}
return this.promise;
}
/**
* Thenable shim — `await ra` resolves to a bare tagged-union `Result<T, E>`.
* Narrow with `isOk(r)` / `isErr(r)` to read `.value` / `.error`.
*/
// biome-ignore lint/suspicious/noThenProperty: makes ResultAsync awaitable
then(onfulfilled, onrejected) {
return this.resolve().then(
// Safe: without onfulfilled, R1 stays at its Result<T, E> default.
(r) => onfulfilled ? onfulfilled(r) : r,
onrejected ?? void 0
);
}
};
// src/async-lift.ts
var okAsync = ResultAsync.ok;
var errAsync = ResultAsync.err;
var fromPromise = ResultAsync.fromPromise;
var fromSafePromise = ResultAsync.fromSafePromise;
var fromResult = ResultAsync.fromResult;
var asyncAfter = dual(
2,
(result, fn) => fromResult(result).flatMap(fn)
);
var fromAsync = (fn, onDefect) => (...args) => ResultAsync.fromResultPromise(fn(...args), onDefect);
var toError = (error) => error instanceof Error ? error : new Error(String(error));
function tryAsync(promise, onReject) {
return onReject ? ResultAsync.fromPromise(promise, onReject) : ResultAsync.fromPromise(promise, toError);
}
// src/collections.ts
var combine = (results) => {
const values = [];
for (const result of results) {
if (isErr(result)) return err(result.error);
values.push(result.value);
}
return ok(values);
};
var combineTuple = (results) => (
// Runtime identical to combine; the cast restores per-index tuple types.
combine(results)
);
function validateAll(results, combineErrors) {
const values = [];
const errors = [];
for (const result of results) {
if (isErr(result)) {
errors.push(result.error);
} else {
values.push(result.value);
}
}
if (errors.length === 0) return ok(values);
return err(combineErrors ? errors.reduce(combineErrors) : errors);
}
function validateTuple(results, combineErrors) {
return combineErrors ? validateAll(results, combineErrors) : validateAll(results);
}
// src/pipe.ts
function flow(first, ...rest) {
return (...args) => {
let acc = first(...args);
for (const fn of rest) {
acc = fn(acc);
}
return acc;
};
}
// src/try-gen.ts
var ErrSignal = class {
constructor(error) {
this.error = error;
}
error;
_tag = "ErrSignal";
};
var yieldResult = (result) => {
if (isErr(result)) {
throw new ErrSignal(result.error);
}
return result.value;
};
var $ = yieldResult;
var tryGen = (fn) => {
try {
return fn();
} catch (error) {
if (error instanceof ErrSignal) {
return err(error.error);
}
throw error;
}
};
exports.$ = $;
exports.ResultAsync = ResultAsync;
exports.UnexpectedError = UnexpectedError;
exports.asyncAfter = asyncAfter;
exports.bimap = bimap;
exports.combine = combine;
exports.combineTuple = combineTuple;
exports.err = err;
exports.errAsync = errAsync;
exports.flatMap = flatMap;
exports.flow = flow;
exports.fromAsync = fromAsync;
exports.fromPromise = fromPromise;
exports.fromResult = fromResult;
exports.fromSafePromise = fromSafePromise;
exports.isErr = isErr;
exports.isOk = isOk;
exports.map = map;
exports.mapErr = mapErr;
exports.match = match;
exports.of = of;
exports.ok = ok;
exports.okAsync = okAsync;
exports.pipe = pipe;
exports.recover = recover;
exports.show = show;
exports.tap = tap;
exports.tapErr = tapErr;
exports.tryAsync = tryAsync;
exports.tryGen = tryGen;
exports.trySync = trySync;
exports.unwrap = unwrap;
exports.unwrapErr = unwrapErr;
exports.unwrapOk = unwrapOk;
exports.unwrapOr = unwrapOr;
exports.validateAll = validateAll;
exports.validateTuple = validateTuple;
exports.yieldResult = yieldResult;
//# sourceMappingURL=index.cjs.map
//# sourceMappingURL=index.cjs.map