UNPKG

@onrails/result

Version:

Tagged Result / ResultAsync for railway-oriented TypeScript — pure tagged unions, neverthrow-shaped compat shim, FL-friendly

625 lines (619 loc) 17.3 kB
// 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; export { CompatResult, CompatResultAsync, Result, CompatResultAsync as ResultAsync, err2 as err, errAsync, fromPromise, fromSafePromise, ok2 as ok, okAsync }; //# sourceMappingURL=neverthrow.js.map //# sourceMappingURL=neverthrow.js.map