https://github.com/alexxandergrib/monads-io
Practical, Tree-Shakeable implementation of Either (Result) and Option (Maybe) in TypeScript
https://github.com/alexxandergrib/monads-io
either either-monad fp maybe maybe-monad monad typescript
Last synced: 5 months ago
JSON representation
Practical, Tree-Shakeable implementation of Either (Result) and Option (Maybe) in TypeScript
- Host: GitHub
- URL: https://github.com/alexxandergrib/monads-io
- Owner: AlexXanderGrib
- License: mit
- Created: 2023-03-28T15:31:32.000Z (about 3 years ago)
- Default Branch: main
- Last Pushed: 2025-03-21T11:03:03.000Z (about 1 year ago)
- Last Synced: 2025-04-29T17:03:39.602Z (about 1 year ago)
- Topics: either, either-monad, fp, maybe, maybe-monad, monad, typescript
- Language: TypeScript
- Homepage: https://npmjs.com/package/monads-io
- Size: 600 KB
- Stars: 2
- Watchers: 1
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Contributing: CONTRIBUTING.md
- License: LICENSE.txt
- Code of conduct: CODE_OF_CONDUCT.md
- Security: SECURITY.md
Awesome Lists containing this project
README
# Monads IO
> 🚀 Efficient Monads for JS: Maybe (Option) and Either (Result)
[](https://github.com/AlexXanderGrib/monads-io)
[](https://npmjs.com/package/monads-io)
[](https://github.com/AlexXanderGrib/monads-io)
[](https://codecov.io/gh/AlexXanderGrib/monads-io)
[](https://github.com/AlexXanderGrib/monads-io)
[](https://snyk.io/advisor/npm-package/monads-io)
[](https://snyk.io/test/npm/monads-io)
[](https://npms.io/search?q=monads-io)
[](https://npmjs.com/package/monads-io)
[](https://github.com/AlexXanderGrib/monads-io/blob/main/LICENSE.txt)
[](https://bundlephobia.com/package/monads-io)
## Why use this lib
1. **Small** and **Tree-Shakable**. Either - 3kb minified, Maybe - 3kb minified, can be imported separately
2. **No dependencies**.
3. **Memory-Efficient**. 8 bytes overhead per instance (only class pointer)
4. **Tested**. 100% coverage
5. **Practical**. Just 2 wrappers: Either and Maybe - easy for non-fp people
## Credits
Huge credit to @JSMonk. This library is based on [`JSMonk/sweet-monads`](https://github.com/JSMonk/sweet-monads)
## Installation
- **Using `npm`**
```shell
npm i monads-io
```
- **Using `Yarn`**
```shell
yarn add monads-io
```
- **Using `pnpm`**
```shell
pnpm add monads-io
```
## Usage
### [Either](./docs/api/modules/either.md)
> The Either type represents values with two possibilities: a value of type Either a b is either Left a or Right b.
> ([source](https://hackage.haskell.org/package/category-extras-0.52.0/docs/Control-Monad-Either.html))
1. Makes error path of function strongly typed
2. Separates errors from exceptions
3. Minimal memory overhead (see [benchmarks](./benchmarks/))
Example (preparing data to render User page)
```typescript
import {
Either,
fromPromise,
fromTryAsync,
left,
mergeInOne,
right
} from "monads-io/either";
class NetworkError extends Error {}
class HttpError extends Error {}
class JsonParsingError extends Error {}
class NotFoundError extends Error {}
type FetchError = NetworkError | HttpError | JsonParsingError;
type ID = string;
type User = { id: ID; username: string; name: string /* ... */ };
type Post = { id: ID; userId: User["id"]; body: string /* ... */ };
async function getJson(url: string): Promise> {
const response = await fromPromise(
fetch(`https://jsonplaceholder.typicode.com/${url}`),
(cause) => new NetworkError("Unable to connect", { cause })
);
const okResponse = response.chain((response) => {
if (response.ok) return right(response);
return left(
new HttpError(
`Response status is ${response.status} ${response.statusText}`,
{ cause: response }
)
);
});
const json = await okResponse.asyncChain((response) => {
return fromTryAsync(
async () => (await response.json()) as T,
(cause) => new JsonParsingError("Unable to parse JSON", { cause })
);
});
return json;
}
async function getUserByUsername(username: string) {
const users = await getJson(`/users?username=${username}`);
return users.chain((users) => {
const user = users[0];
if (!user) {
return left(new NotFoundError(`User not found`, { cause: { username } }));
}
return right(user);
});
}
const getPosts = (userId: string) =>
getJson(`/posts?ownerId=${userId}`);
class PageLoadError extends Error {
/* ... */
constructor(public returnStatus: number, message: string, cause?: unknown) {
super(message, { cause });
}
}
async function getUserPageData(username: string) {
const user = await getUserByUsername(username);
const posts = await user.asyncChain((user) => getPosts(user.id));
return mergeInOne([user, posts])
.map(([user, posts]) => ({ user, posts }))
.mapLeft((error) => {
if (error instanceof NotFoundError) {
return new PageLoadError(404, "User not found", error);
}
// error: FetchError
console.log("Error fetching data for User Page", error);
return new PageLoadError(500, "Internal server error", error);
});
}
```
### [Maybe](./docs/api/modules/maybe.md)
> The Maybe monad represents computations which might "go wrong" by not returning a value.
> ([source](https://en.wikibooks.org/wiki/Haskell/Understanding_monads/Maybe))
1. Allows to separate empty/present state from undefined
2. Minimal memory overhead (see [benchmarks](./benchmarks/))
Example (searching mention target)
```typescript
// Real world example
// This maybe is not tree-shakable. Used in NodeJS code
import * as Maybe from "monads-io/maybe";
export async function getTargets(
api: TelegramAPI,
tokens: formattedText,
{ mentionLimit = 1, message = undefined as message | undefined } = {}
): Promise> {
const mentions = getMentions(tokens).slice(0, mentionLimit);
const targets = new Map();
let replyTarget: [number, chat | undefined] | undefined;
const { messagesService, chatsService } = getServices(api);
...
// 1. Get message
// 2. Get message reply id (0 = no reply)
// 3. Get reply message by message id
// 4. Get reply message sender
// 5. Get his/her profile
// 6. Set local variable to profile
const reply = await Maybe.fromNullable(message)
.filter((message) => message.reply_to_message_id !== 0)
.asyncChain((message) =>
messagesService.getReply(message.chat_id, message.id)
);
const sender = await reply
.map(MemberId.fromMessage)
.tap(({ memberId }) => {
replyTarget = [memberId, undefined];
})
.asyncChain(({ memberId }) => chatsService.getById(memberId));
sender.tap((sender) => {
replyTarget = [sender.id, sender];
});
...
return replyTarget ? new Map([replyTarget, ...targets]) : targets;
}
```
### [Identity](./docs/api/modules/identity.md)
> The Identity monad is a monad that does not embody any computational strategy. It simply applies the bound function to its input without any modification.
> ([source](https://blog.ploeh.dk/2022/05/16/the-identity-monad/))
Example
```typescript
import * as Identity from "monads-io/identity";
// Before
app.use(express.static(path.resolve(getDirname(import.meta.url), "../public")));
// After
Identity.from(import.meta.url)
.map(getDirname)
.map((dir) => path.resolve(dir, "../public"))
.map(express.static)
.map(app.use);
```