@onrails/result
Version:
Tagged Result / ResultAsync for railway-oriented TypeScript — pure tagged unions, neverthrow-shaped compat shim, FL-friendly
636 lines (629 loc) • 17.4 kB
JavaScript
'use strict';
// 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 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 flatMap = dual(
2,
(result, fn) => isOk(result) ? fn(result.value) : err(result.error)
);
var unwrapOr = dual(
2,
(result, defaultValue) => isOk(result) ? result.value : defaultValue
);
function trySync(fn, onThrow) {
return (...args) => {
try {
return ok(fn(...args));
} catch (error) {
return err(onThrow(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/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)
);
// src/compat/neverthrow.ts
var CompatResult = class _CompatResult {
constructor(inner) {
this.inner = inner;
}
inner;
static fromThrowable(fn, onThrow) {
const wrapped = trySync(fn, onThrow);
return (...args) => new _CompatResult(wrapped(...args));
}
static combine(results) {
return new _CompatResult(
combineTuple(results.map((r) => r.inner))
);
}
get value() {
return this._unsafeUnwrap();
}
get error() {
return this._unsafeUnwrapErr();
}
isOk() {
return isOk(this.inner);
}
isErr() {
return isErr(this.inner);
}
map(fn) {
return new _CompatResult(map(this.inner, fn));
}
mapErr(fn) {
return new _CompatResult(mapErr(this.inner, fn));
}
andThen(fn) {
return new _CompatResult(flatMap(this.inner, (value) => fn(value).inner));
}
asyncAndThen(fn) {
return isErr(this.inner) ? CompatResultAsync.err(this.inner.error) : CompatResultAsync.fromInner(coerceToCoreAsync(fn(this.inner.value)));
}
orElse(fn) {
return isOk(this.inner) ? new _CompatResult(this.inner) : fn(this.inner.error);
}
match(onOk, onErr) {
return isOk(this.inner) ? onOk(this.inner.value) : onErr(this.inner.error);
}
unwrapOr(defaultValue) {
return unwrapOr(this.inner, defaultValue);
}
_unsafeUnwrap() {
if (isErr(this.inner)) {
throw new Error("Called _unsafeUnwrap on Err");
}
return this.inner.value;
}
_unsafeUnwrapErr() {
if (isOk(this.inner)) {
throw new Error("Called _unsafeUnwrapErr on Ok");
}
return this.inner.error;
}
};
var Result = CompatResult;
var ok2 = (value) => new CompatResult(ok(value));
var err2 = (error) => new CompatResult(err(error));
var CompatResultAsync = class _CompatResultAsync {
constructor(inner) {
this.inner = inner;
}
inner;
static fromInner(inner) {
return new _CompatResultAsync(inner);
}
toCore() {
return this.inner;
}
static ok(value) {
return new _CompatResultAsync(ResultAsync.ok(value));
}
static err(error) {
return new _CompatResultAsync(ResultAsync.err(error));
}
static fromPromise(promise, onReject) {
return new _CompatResultAsync(ResultAsync.fromPromise(promise, onReject));
}
static fromSafePromise(promise) {
return new _CompatResultAsync(ResultAsync.fromSafePromise(promise));
}
static fromThrowable(fn, onThrow) {
return (...args) => _CompatResultAsync.fromPromise(fn(...args), onThrow);
}
static combine(results) {
return new _CompatResultAsync(
ResultAsync.combine(results.map((r) => r.inner))
);
}
// biome-ignore lint/suspicious/noThenProperty: thenable shim for `await ra` -> CompatResult
then(onfulfilled, onrejected) {
return this.inner.resolve().then((r) => {
const wrapped = new CompatResult(r);
return onfulfilled ? onfulfilled(wrapped) : wrapped;
}, onrejected ?? void 0);
}
resolve() {
return this.inner.resolve();
}
map(fn) {
return new _CompatResultAsync(this.inner.map(fn));
}
mapErr(fn) {
return new _CompatResultAsync(this.inner.mapErr(fn));
}
andThen(fn) {
return new _CompatResultAsync(this.inner.andThen((value) => coerceToCore(fn(value))));
}
chain(fn) {
return this.andThen(fn);
}
flatMap(fn) {
return this.andThen(fn);
}
orElse(fn) {
return new _CompatResultAsync(
this.inner.orElse(
(error) => coerceToCore(fn(error))
)
);
}
unwrapOr(defaultValue) {
return this.inner.unwrapOr(defaultValue);
}
isOk() {
return this.inner.match(
() => true,
() => false
);
}
isErr() {
return this.inner.match(
() => false,
() => true
);
}
match(onOk, onErr) {
return this.inner.match(onOk, onErr);
}
andTee(fn) {
return new _CompatResultAsync(
ResultAsync.defer(async () => {
const r = await this.inner.resolve();
if (!isErr(r)) {
try {
await fn(r.value);
} catch {
}
}
return r;
})
);
}
orTee(fn) {
return new _CompatResultAsync(
ResultAsync.defer(async () => {
const r = await this.inner.resolve();
if (isErr(r)) {
try {
await fn(r.error);
} catch {
}
}
return r;
})
);
}
};
function coerceToCore(next) {
if (next instanceof CompatResultAsync) return next.toCore();
if (next instanceof ResultAsync) return next;
if (next instanceof CompatResult) return next.inner;
return next;
}
var coerceToCoreAsync = (next) => next instanceof CompatResultAsync ? next.toCore() : next;
var okAsync = CompatResultAsync.ok;
var errAsync = CompatResultAsync.err;
var fromPromise = CompatResultAsync.fromPromise;
var fromSafePromise = CompatResultAsync.fromSafePromise;
exports.CompatResult = CompatResult;
exports.CompatResultAsync = CompatResultAsync;
exports.Result = Result;
exports.ResultAsync = CompatResultAsync;
exports.err = err2;
exports.errAsync = errAsync;
exports.fromPromise = fromPromise;
exports.fromSafePromise = fromSafePromise;
exports.ok = ok2;
exports.okAsync = okAsync;
//# sourceMappingURL=neverthrow.cjs.map
//# sourceMappingURL=neverthrow.cjs.map