@rafikidota/monadia
Sometimes, the best way to solve your own problems is to help someone else.
Monads for TypeScript. Zero dependencies, dual CJS/ESM.
npm install @rafikidota/monadia
ResultSync
Holds either a value R or an error E — never both, never neither.
import { ResultSync } from '@rafikidota/monadia';
function parse(raw: string): ResultSync<number, RangeError> {
const port = Number(raw);
if (!Number.isInteger(port) || port < 1 || port > 65535) {
return ResultSync.Fail(new RangeError(`invalid port: ${raw}`));
}
return ResultSync.Ok(port);
}
parse('8080')
.map((port) => `http://localhost:${port}`)
.unwrapOr('http://localhost:3000');
ResultSync.Ok() with no argument is ResultSync<void, E>. The class is final: the
constructor is private and instances are frozen. To extend behaviour, wrap it.
private is a compile-time guarantee, so the constructor also validates its
state at runtime for JavaScript consumers.
| Group | Members |
|---|---|
| Construction | Ok, Fail (static) |
| State | successful |
| Access | getValue, getError |
| Transform | map, mapOr, mapOrElse, mapOrError |
| Compose | and, andThen, or, orElse |
| Extract | expect, unwrap, unwrapOr, unwrapOrElse, unwrapError |
ResultAsync
Wraps a Promise<ResultSync<R, E>>. Combinators return a new ResultAsync, so a chain
reads as one expression.
import { ResultAsync } from '@rafikidota/monadia';
const view = await ResultAsync.fromPromise(
repo.findOneOrFail(id),
(e) => new NotFoundError(String(e)),
)
.andThen(loadOrders)
.map(format)
.unwrapOr(EMPTY);
| Group | Members | Returns |
|---|---|---|
| Construction | Ok, Fail (static) |
ResultAsync |
| Conversion | from, fromPromise (static) |
ResultAsync |
| Chainable | map, mapOrError, and, andThen, or, orElse |
ResultAsync |
| Terminal | getValue, getError, mapOr, mapOrElse, expect, unwrap, unwrapOr, unwrapOrElse, unwrapError |
Promise |
| Thenable | then |
await ra gives a ResultSync |
Callbacks can be sync or async, and andThen accepts a function returning a ResultSync,
a ResultAsync or a Promise<ResultSync>, so sync and async steps mix in one chain.
Notes
mapOrElse(def, func)takes the error callback first, then the value callback, on both classes.- Rejections propagate. A callback that throws inside
map/andThenrejects the resulting promise; it is not converted into aFail.fromPromiseis the only entry point from throwing code — itsonErris what keepsEhonest.fromtakes a promise that already resolves to aResultSyncand assumes it does not reject. - No
*Asyncmethods onResultSyncby design: a method returningPromise<ResultSync<T, E>>is not aResultSync, so it would not compose.
License
MIT