{"id":20514431,"url":"https://github.com/alexxandergrib/monads-io","last_synced_at":"2026-01-06T19:03:45.815Z","repository":{"id":148563064,"uuid":"620390639","full_name":"AlexXanderGrib/monads-io","owner":"AlexXanderGrib","description":"Practical, Tree-Shakeable implementation of Either (Result) and Option (Maybe) in TypeScript","archived":false,"fork":false,"pushed_at":"2025-03-21T11:03:03.000Z","size":614,"stargazers_count":2,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-04-29T17:03:39.602Z","etag":null,"topics":["either","either-monad","fp","maybe","maybe-monad","monad","typescript"],"latest_commit_sha":null,"homepage":"https://npmjs.com/package/monads-io","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/AlexXanderGrib.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE.txt","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-03-28T15:31:32.000Z","updated_at":"2025-03-21T10:48:00.000Z","dependencies_parsed_at":"2024-06-28T09:26:19.183Z","dependency_job_id":null,"html_url":"https://github.com/AlexXanderGrib/monads-io","commit_stats":null,"previous_names":[],"tags_count":3,"template":false,"template_full_name":"AlexXanderGrib/package-template","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AlexXanderGrib%2Fmonads-io","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AlexXanderGrib%2Fmonads-io/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AlexXanderGrib%2Fmonads-io/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AlexXanderGrib%2Fmonads-io/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/AlexXanderGrib","download_url":"https://codeload.github.com/AlexXanderGrib/monads-io/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":253217272,"owners_count":21873055,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["either","either-monad","fp","maybe","maybe-monad","monad","typescript"],"created_at":"2024-11-15T21:16:22.612Z","updated_at":"2026-01-06T19:03:40.779Z","avatar_url":"https://github.com/AlexXanderGrib.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Monads IO\n\n\u003e 🚀 Efficient Monads for JS: Maybe (Option) and Either (Result)\n\n[![Test Status](https://github.com/AlexXanderGrib/monads-io/actions/workflows/test.yml/badge.svg)](https://github.com/AlexXanderGrib/monads-io)\n[![Downloads](https://img.shields.io/npm/dt/monads-io.svg)](https://npmjs.com/package/monads-io)\n[![last commit](https://img.shields.io/github/last-commit/AlexXanderGrib/monads-io.svg)](https://github.com/AlexXanderGrib/monads-io)\n[![codecov](https://img.shields.io/codecov/c/github/AlexXanderGrib/monads-io/main.svg)](https://codecov.io/gh/AlexXanderGrib/monads-io)\n[![GitHub](https://img.shields.io/github/stars/AlexXanderGrib/monads-io.svg)](https://github.com/AlexXanderGrib/monads-io)\n[![monads-io](https://snyk.io/advisor/npm-package/monads-io/badge.svg)](https://snyk.io/advisor/npm-package/monads-io)\n[![Known Vulnerabilities](https://snyk.io/test/npm/monads-io/badge.svg)](https://snyk.io/test/npm/monads-io)\n[![Quality](https://img.shields.io/npms-io/quality-score/monads-io.svg?label=quality%20%28npms.io%29\u0026)](https://npms.io/search?q=monads-io)\n[![npm](https://img.shields.io/npm/v/monads-io.svg)](https://npmjs.com/package/monads-io)\n[![license MIT](https://img.shields.io/npm/l/monads-io.svg)](https://github.com/AlexXanderGrib/monads-io/blob/main/LICENSE.txt)\n[![Size](https://img.shields.io/bundlephobia/minzip/monads-io)](https://bundlephobia.com/package/monads-io)\n\n## Why use this lib\n\n1. **Small** and **Tree-Shakable**. Either - 3kb minified, Maybe - 3kb minified, can be imported separately\n2. **No dependencies**.\n3. **Memory-Efficient**. 8 bytes overhead per instance (only class pointer)\n4. **Tested**. 100% coverage\n5. **Practical**. Just 2 wrappers: Either and Maybe - easy for non-fp people\n\n## Credits\n\nHuge credit to @JSMonk. This library is based on [`JSMonk/sweet-monads`](https://github.com/JSMonk/sweet-monads)\n\n## Installation\n\n- **Using `npm`**\n  ```shell\n  npm i monads-io\n  ```\n- **Using `Yarn`**\n  ```shell\n  yarn add monads-io\n  ```\n- **Using `pnpm`**\n  ```shell\n  pnpm add monads-io\n  ```\n\n## Usage\n\n### [Either](./docs/api/modules/either.md)\n\n\u003e The Either type represents values with two possibilities: a value of type Either a b is either Left a or Right b.\n\u003e ([source](https://hackage.haskell.org/package/category-extras-0.52.0/docs/Control-Monad-Either.html))\n\n1. Makes error path of function strongly typed\n2. Separates errors from exceptions\n3. Minimal memory overhead (see [benchmarks](./benchmarks/))\n\n\u003cdetails\u003e\n  \u003csummary\u003eExample (preparing data to render User page)\u003c/summary\u003e\n\n```typescript\nimport {\n  Either,\n  fromPromise,\n  fromTryAsync,\n  left,\n  mergeInOne,\n  right\n} from \"monads-io/either\";\n\nclass NetworkError extends Error {}\nclass HttpError extends Error {}\nclass JsonParsingError extends Error {}\nclass NotFoundError extends Error {}\n\ntype FetchError = NetworkError | HttpError | JsonParsingError;\n\ntype ID = string;\ntype User = { id: ID; username: string; name: string /* ... */ };\ntype Post = { id: ID; userId: User[\"id\"]; body: string /* ... */ };\n\nasync function getJson\u003cT\u003e(url: string): Promise\u003cEither\u003cFetchError, T\u003e\u003e {\n  const response = await fromPromise(\n    fetch(`https://jsonplaceholder.typicode.com/${url}`),\n    (cause) =\u003e new NetworkError(\"Unable to connect\", { cause })\n  );\n\n  const okResponse = response.chain((response) =\u003e {\n    if (response.ok) return right(response);\n\n    return left(\n      new HttpError(\n        `Response status is ${response.status} ${response.statusText}`,\n        { cause: response }\n      )\n    );\n  });\n\n  const json = await okResponse.asyncChain((response) =\u003e {\n    return fromTryAsync(\n      async () =\u003e (await response.json()) as T,\n      (cause) =\u003e new JsonParsingError(\"Unable to parse JSON\", { cause })\n    );\n  });\n\n  return json;\n}\n\nasync function getUserByUsername(username: string) {\n  const users = await getJson\u003cUser[]\u003e(`/users?username=${username}`);\n\n  return users.chain((users) =\u003e {\n    const user = users[0];\n\n    if (!user) {\n      return left(new NotFoundError(`User not found`, { cause: { username } }));\n    }\n\n    return right(user);\n  });\n}\n\nconst getPosts = (userId: string) =\u003e\n  getJson\u003cPost[]\u003e(`/posts?ownerId=${userId}`);\n\nclass PageLoadError extends Error {\n  /* ... */\n\n  constructor(public returnStatus: number, message: string, cause?: unknown) {\n    super(message, { cause });\n  }\n}\n\nasync function getUserPageData(username: string) {\n  const user = await getUserByUsername(username);\n  const posts = await user.asyncChain((user) =\u003e getPosts(user.id));\n\n  return mergeInOne([user, posts])\n    .map(([user, posts]) =\u003e ({ user, posts }))\n    .mapLeft((error) =\u003e {\n      if (error instanceof NotFoundError) {\n        return new PageLoadError(404, \"User not found\", error);\n      }\n\n      // error: FetchError\n      console.log(\"Error fetching data for User Page\", error);\n      return new PageLoadError(500, \"Internal server error\", error);\n    });\n}\n```\n\n\u003c/details\u003e\n\n### [Maybe](./docs/api/modules/maybe.md)\n\n\u003e The Maybe monad represents computations which might \"go wrong\" by not returning a value.\n\u003e ([source](https://en.wikibooks.org/wiki/Haskell/Understanding_monads/Maybe))\n\n1. Allows to separate empty/present state from undefined\n2. Minimal memory overhead (see [benchmarks](./benchmarks/))\n\n\u003cdetails\u003e\n  \u003csummary\u003eExample (searching mention target)\u003c/summary\u003e\n\n```typescript\n// Real world example\n// This maybe is not tree-shakable. Used in NodeJS code\nimport * as Maybe from \"monads-io/maybe\";\n\nexport async function getTargets(\napi: TelegramAPI,\ntokens: formattedText,\n{ mentionLimit = 1, message = undefined as message | undefined } = {}\n): Promise\u003cMap\u003cnumber, chat | undefined\u003e\u003e {\nconst mentions = getMentions(tokens).slice(0, mentionLimit);\n\nconst targets = new Map\u003cnumber, chat | undefined\u003e();\nlet replyTarget: [number, chat | undefined] | undefined;\nconst { messagesService, chatsService } = getServices(api);\n\n...\n\n// 1. Get message\n// 2. Get message reply id (0 = no reply)\n// 3. Get reply message by message id\n// 4. Get reply message sender\n// 5. Get his/her profile\n// 6. Set local variable to profile\n\nconst reply = await Maybe.fromNullable(message)\n  .filter((message) =\u003e message.reply_to_message_id !== 0)\n  .asyncChain((message) =\u003e\n    messagesService.getReply(message.chat_id, message.id)\n  );\n\nconst sender = await reply\n  .map(MemberId.fromMessage)\n  .tap(({ memberId }) =\u003e {\n    replyTarget = [memberId, undefined];\n  })\n  .asyncChain(({ memberId }) =\u003e chatsService.getById(memberId));\n\nsender.tap((sender) =\u003e {\n  replyTarget = [sender.id, sender];\n});\n\n...\n\nreturn replyTarget ? new Map([replyTarget, ...targets]) : targets;\n}\n\n```\n\n\u003c/details\u003e\n\n### [Identity](./docs/api/modules/identity.md)\n\n\u003e 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.\n\u003e ([source](https://blog.ploeh.dk/2022/05/16/the-identity-monad/))\n\nExample\n\n```typescript\nimport * as Identity from \"monads-io/identity\";\n\n// Before\napp.use(express.static(path.resolve(getDirname(import.meta.url), \"../public\")));\n\n// After\nIdentity.from(import.meta.url)\n  .map(getDirname)\n  .map((dir) =\u003e path.resolve(dir, \"../public\"))\n  .map(express.static)\n  .map(app.use);\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Falexxandergrib%2Fmonads-io","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Falexxandergrib%2Fmonads-io","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Falexxandergrib%2Fmonads-io/lists"}