├── .babelrc ├── .changeset └── config.json ├── .eslintrc.js ├── .github └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── .prettierrc.js ├── CHANGELOG.md ├── CODEOWNERS ├── LICENSE ├── README.md ├── jest.config.js ├── package-lock.json ├── package.json ├── rollup.config.mjs ├── src ├── _internals │ ├── error.ts │ └── utils.ts ├── index.ts ├── result-async.ts └── result.ts ├── tests ├── index.test.ts ├── safe-try.test.ts ├── tsconfig.tests.json └── typecheck-tests.ts └── tsconfig.json /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env", "@babel/preset-typescript"] 3 | } 4 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json", 3 | "changelog": [ 4 | "@changesets/changelog-github", 5 | { 6 | "repo": "supermacro/neverthrow" 7 | } 8 | ], 9 | "commit": false, 10 | "fixed": [], 11 | "linked": [], 12 | "access": "public", 13 | "baseBranch": "master", 14 | "updateInternalDependencies": "patch", 15 | "ignore": [] 16 | } 17 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: '@typescript-eslint/parser', 4 | env: { 5 | node: true, 6 | }, 7 | ignorePatterns: ['dist/', 'tests/'], 8 | plugins: [ 9 | '@typescript-eslint', 10 | ], 11 | extends: [ 12 | 'eslint:recommended', 13 | 'plugin:@typescript-eslint/eslint-recommended', 14 | 'plugin:@typescript-eslint/recommended', 15 | 'prettier/@typescript-eslint', 16 | 'plugin:prettier/recommended' 17 | ], 18 | rules: { 19 | semi: 'off', 20 | '@typescript-eslint/no-explicit-any': 'error', 21 | '@typescript-eslint/semi': 'off', 22 | '@typescript-eslint/explicit-function-return-type': 'off', 23 | '@typescript-eslint/no-unused-vars': 'off', 24 | '@typescript-eslint/interface-name-prefix': 'off', 25 | '@typescript-eslint/member-delimiter-style': ['error', { 26 | multiline: { 27 | delimiter: 'none', 28 | }, 29 | singleline: { 30 | delimiter: 'semi', 31 | requireLast: false 32 | } 33 | }] 34 | }, 35 | }; 36 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | pull_request: 4 | push: 5 | branches: 6 | - master 7 | concurrency: 8 | group: ${{ github.workflow }}-${{ github.head_ref || github.ref }} 9 | cancel-in-progress: true 10 | 11 | env: 12 | RUNNER_NODE_VERSION: 22 13 | jobs: 14 | install_deps: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v4 18 | - name: Use Node.js ${{ env.RUNNER_NODE_VERSION }} 19 | uses: actions/setup-node@v4 20 | with: 21 | node-version: ${{ env.RUNNER_NODE_VERSION }} 22 | - name: cache dependencies 23 | uses: actions/cache@v4 24 | id: cache-dependencies 25 | with: 26 | path: "node_modules" 27 | key: depends-node${{ env.RUNNER_NODE_VERSION }}-${{ hashFiles('package-lock.json') }} 28 | - name: install dependencies 29 | if: steps.cache-dependencies.outputs.cache-hit != 'true' 30 | run: npm i 31 | 32 | typecheck: 33 | runs-on: ubuntu-latest 34 | needs: install_deps 35 | steps: 36 | - uses: actions/checkout@v4 37 | - name: Use Node.js ${{ env.RUNNER_NODE_VERSION }} 38 | uses: actions/setup-node@v4 39 | with: 40 | node-version: ${{ env.RUNNER_NODE_VERSION }} 41 | - name: restore dependencies cache 42 | uses: actions/cache/restore@v4 43 | with: 44 | path: "node_modules" 45 | key: depends-node${{ env.RUNNER_NODE_VERSION }}-${{ hashFiles('package-lock.json') }} 46 | - name: typecheck 47 | run: npm run typecheck 48 | 49 | lint: 50 | runs-on: ubuntu-latest 51 | needs: install_deps 52 | steps: 53 | - uses: actions/checkout@v4 54 | - name: Use Node.js ${{ env.RUNNER_NODE_VERSION }} 55 | uses: actions/setup-node@v4 56 | with: 57 | node-version: ${{ env.RUNNER_NODE_VERSION }} 58 | - name: restore dependencies cache 59 | uses: actions/cache/restore@v4 60 | with: 61 | path: "node_modules" 62 | key: depends-node${{ env.RUNNER_NODE_VERSION }}-${{ hashFiles('package-lock.json') }} 63 | - name: lint 64 | run: npm run lint 65 | 66 | build: 67 | runs-on: ubuntu-latest 68 | needs: install_deps 69 | steps: 70 | - uses: actions/checkout@v4 71 | - name: Use Node.js ${{ env.RUNNER_NODE_VERSION }} 72 | uses: actions/setup-node@v4 73 | with: 74 | node-version: ${{ env.RUNNER_NODE_VERSION }} 75 | - name: restore dependencies cache 76 | uses: actions/cache/restore@v4 77 | with: 78 | path: "node_modules" 79 | key: depends-node${{ env.RUNNER_NODE_VERSION }}-${{ hashFiles('package-lock.json') }} 80 | - name: build 81 | run: npm run build 82 | 83 | test: 84 | runs-on: ubuntu-latest 85 | needs: install_deps 86 | strategy: 87 | matrix: 88 | node-version: [18, 20, 22] 89 | steps: 90 | - uses: actions/checkout@v4 91 | - name: Use Node.js ${{ matrix.node-version }} 92 | uses: actions/setup-node@v4 93 | with: 94 | node-version: ${{ matrix.node-version }} 95 | - name: restore dependencies cache 96 | uses: actions/cache/restore@v4 97 | with: 98 | path: "node_modules" 99 | key: depends-node${{ env.RUNNER_NODE_VERSION }}-${{ hashFiles('package-lock.json') }} 100 | - name: test 101 | run: npm run test 102 | test_result: 103 | runs-on: ubuntu-latest 104 | needs: test 105 | if: ${{ always() }} 106 | steps: 107 | - run: exit 1 108 | if: ${{ needs.test.result != 'success' }} 109 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | concurrency: ${{ github.workflow }}-${{ github.ref }} 9 | 10 | jobs: 11 | create_pr: 12 | name: Release 13 | runs-on: ubuntu-latest 14 | outputs: 15 | hasChangesets: ${{ steps.changesets.outputs.hasChangesets }} 16 | steps: 17 | - uses: actions/checkout@v4 18 | 19 | - uses: actions/setup-node@v4 20 | with: 21 | node-version: 22 22 | cache: 'npm' 23 | 24 | - run: npm i 25 | 26 | - name: Create Release Pull Request 27 | id: changesets 28 | uses: changesets/action@v1 29 | with: 30 | version: npm run version 31 | env: 32 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 33 | 34 | release: 35 | needs: create_pr 36 | if: needs.create_pr.outputs.hasChangesets == 'false' 37 | runs-on: ubuntu-latest 38 | environment: deploy 39 | steps: 40 | - uses: actions/checkout@v4 41 | 42 | - uses: actions/setup-node@v4 43 | with: 44 | node-version: 22 45 | cache: 'npm' 46 | 47 | - run: npm i 48 | 49 | - name: release 50 | id: changesets 51 | uses: changesets/action@v1 52 | with: 53 | publish: npm run release 54 | env: 55 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 56 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 57 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | tmp/ 3 | node_modules/ 4 | publish.sh -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: false, 3 | trailingComma: "all", 4 | singleQuote: true, 5 | printWidth: 100, 6 | tabWidth: 2 7 | }; 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # neverthrow 2 | 3 | ## 8.2.0 4 | 5 | ### Minor Changes 6 | 7 | - [#615](https://github.com/supermacro/neverthrow/pull/615) [`85ed7fd`](https://github.com/supermacro/neverthrow/commit/85ed7fd3a1247e4c0e83bba13f5e874282243d75) Thanks [@konker](https://github.com/konker)! - Add orTee, which is the equivalent of andTee but for the error track. 8 | 9 | - [#584](https://github.com/supermacro/neverthrow/pull/584) [`acea44a`](https://github.com/supermacro/neverthrow/commit/acea44adb98dda2ca32fe4e882879461cc7cedc2) Thanks [@macksal](https://github.com/macksal)! - Allow ok/err/okAsync/errAsync to accept zero arguments when returning void 10 | 11 | ## 8.1.1 12 | 13 | ### Patch Changes 14 | 15 | - [#600](https://github.com/supermacro/neverthrow/pull/600) [`3aee20a`](https://github.com/supermacro/neverthrow/commit/3aee20a1c429062d26f440fde32a3f26ef05533a) Thanks [@m-shaka](https://github.com/m-shaka)! - docs: updated README.md about `safeTry` and added @deprecated tag to safeUnwrap 16 | 17 | ## 8.1.0 18 | 19 | ### Minor Changes 20 | 21 | - [#589](https://github.com/supermacro/neverthrow/pull/589) [`609b398`](https://github.com/supermacro/neverthrow/commit/609b398aa1fd258a1fede974707d54eb4c230f3c) Thanks [@dmmulroy](https://github.com/dmmulroy)! - safeTry should not require .safeUnwrap() 22 | 23 | ## 8.0.0 24 | 25 | ### Major Changes 26 | 27 | - [#484](https://github.com/supermacro/neverthrow/pull/484) [`09faf35`](https://github.com/supermacro/neverthrow/commit/09faf35a5ce701ed55b13b82074da9e50050526d) Thanks [@braxtonhall](https://github.com/braxtonhall)! - Allow orElse method to change ok types. 28 | This makes the orElse types match the implementation. 29 | 30 | This is a breaking change for the orElse type argument list, 31 | as the ok type must now be provided before the err type. 32 | 33 | ```diff 34 | - result.orElse(foo) 35 | + result.orElse(foo) 36 | ``` 37 | 38 | This only applies if type arguments were 39 | explicitly provided at an orElse callsite. 40 | If the type arguments were inferred, 41 | no updates are needed during the upgrade. 42 | 43 | ## 7.2.0 44 | 45 | ### Minor Changes 46 | 47 | - [#562](https://github.com/supermacro/neverthrow/pull/562) [`547352f`](https://github.com/supermacro/neverthrow/commit/547352f326206b2c5b403bde4ddc88825172f25c) Thanks [@sharno](https://github.com/sharno)! - change the return type of `safeTry` to be `ResultAsync` instead of `Promise>` for better composability 48 | 49 | ## 7.1.0 50 | 51 | ### Minor Changes 52 | 53 | - [#467](https://github.com/supermacro/neverthrow/pull/467) [`4b9d2fd`](https://github.com/supermacro/neverthrow/commit/4b9d2fdaf03223945068509f948b57194732aa03) Thanks [@untidy-hair 54 | ](https://github.com/untidy-hair)! - feat: add `andTee` and `andThrough` to handle side-effect 55 | 56 | ### Patch Changes 57 | 58 | - [#483](https://github.com/supermacro/neverthrow/pull/483) [`96f7f66`](https://github.com/supermacro/neverthrow/commit/96f7f669ac83be705a389d47ed804e9d44a13932) Thanks [@braxtonhall](https://github.com/braxtonhall)! - Fix `combineWithAllErrors` types 59 | 60 | - [#563](https://github.com/supermacro/neverthrow/pull/563) [`eadf50c`](https://github.com/supermacro/neverthrow/commit/eadf50c695db896b8841c0ee301ae5eeba994b90) Thanks [@mattpocock](https://github.com/mattpocock)! - Made err() infer strings narrowly for easier error tagging. 61 | 62 | ## 7.0.1 63 | 64 | ### Patch Changes 65 | 66 | - [#527](https://github.com/supermacro/neverthrow/pull/527) [`2e1f198`](https://github.com/supermacro/neverthrow/commit/2e1f19899800ce5e1164412c6a693cf2f1c40b20) Thanks [@3846masa](https://github.com/3846masa)! - fix: change type definitions to make inferring types of safeTry more strict 67 | 68 | - [#497](https://github.com/supermacro/neverthrow/pull/497) [`e06203e`](https://github.com/supermacro/neverthrow/commit/e06203e90b2b64edaa42707cbca8383c9f4765e8) Thanks [@braxtonhall](https://github.com/braxtonhall)! - enhance type inferrence of `match` 69 | 70 | ## 7.0.0 71 | 72 | ### Major Changes 73 | 74 | - [#553](https://github.com/supermacro/neverthrow/pull/553) [`5a3af0a`](https://github.com/supermacro/neverthrow/commit/5a3af0a55d0c440dfd50bfbbe021c6e4b973184b) Thanks [@m-shaka](https://github.com/m-shaka)! - Declare the minimum supported Node.js version 75 | 76 | `Neverthrow` does not depend on any Node.js version-specific features, so it should work with any version of Node.js that supports ES6 and other runtimes like Browser, Deno, etc. 77 | 78 | However, for the sake of maintaining a consistent development environment, we should declare the minimum supported version of Node.js in the `engines` field of the `package.json` file. 79 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @m-shaka @supermacro 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Giorgio Delgado 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NeverThrow 🙅 2 | 3 | [![GitHub Workflow Status](https://github.com/supermacro/neverthrow/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/supermacro/neverthrow/actions) 4 | 5 | ## Description 6 | 7 | Encode failure into your program. 8 | 9 | This package contains a `Result` type that represents either success (`Ok`) or failure (`Err`). 10 | 11 | For asynchronous tasks, `neverthrow` offers a `ResultAsync` class which wraps a `Promise>` and gives you the same level of expressivity and control as a regular `Result`. 12 | 13 | `ResultAsync` is `thenable` meaning it **behaves exactly like a native `Promise`** ... except you have access to the same methods that `Result` provides without having to `await` or `.then` the promise! Check out [the wiki](https://github.com/supermacro/neverthrow/wiki/Basic-Usage-Examples#asynchronous-api) for examples and best practices. 14 | 15 | > Need to see real-life examples of how to leverage this package for error handling? See this repo: https://github.com/parlez-vous/server 16 | 17 |
18 | 19 | ## Table Of Contents 20 | 21 | * [Installation](#installation) 22 | * [Recommended: Use `eslint-plugin-neverthrow`](#recommended-use-eslint-plugin-neverthrow) 23 | * [Top-Level API](#top-level-api) 24 | * [API Documentation](#api-documentation) 25 | + [Synchronous API (`Result`)](#synchronous-api-result) 26 | - [`ok`](#ok) 27 | - [`err`](#err) 28 | - [`Result.isOk` (method)](#resultisok-method) 29 | - [`Result.isErr` (method)](#resultiserr-method) 30 | - [`Result.map` (method)](#resultmap-method) 31 | - [`Result.mapErr` (method)](#resultmaperr-method) 32 | - [`Result.unwrapOr` (method)](#resultunwrapor-method) 33 | - [`Result.andThen` (method)](#resultandthen-method) 34 | - [`Result.asyncAndThen` (method)](#resultasyncandthen-method) 35 | - [`Result.orElse` (method)](#resultorelse-method) 36 | - [`Result.match` (method)](#resultmatch-method) 37 | - [`Result.asyncMap` (method)](#resultasyncmap-method) 38 | - [`Result.andTee` (method)](#resultandtee-method) 39 | - [`Result.orTee` (method)](#resultortee-method) 40 | - [`Result.andThrough` (method)](#resultandthrough-method) 41 | - [`Result.asyncAndThrough` (method)](#resultasyncandthrough-method) 42 | - [`Result.fromThrowable` (static class method)](#resultfromthrowable-static-class-method) 43 | - [`Result.combine` (static class method)](#resultcombine-static-class-method) 44 | - [`Result.combineWithAllErrors` (static class method)](#resultcombinewithallerrors-static-class-method) 45 | - [`Result.safeUnwrap()`](#resultsafeunwrap) 46 | + [Asynchronous API (`ResultAsync`)](#asynchronous-api-resultasync) 47 | - [`okAsync`](#okasync) 48 | - [`errAsync`](#errasync) 49 | - [`ResultAsync.fromThrowable` (static class method)](#resultasyncfromthrowable-static-class-method) 50 | - [`ResultAsync.fromPromise` (static class method)](#resultasyncfrompromise-static-class-method) 51 | - [`ResultAsync.fromSafePromise` (static class method)](#resultasyncfromsafepromise-static-class-method) 52 | - [`ResultAsync.map` (method)](#resultasyncmap-method) 53 | - [`ResultAsync.mapErr` (method)](#resultasyncmaperr-method) 54 | - [`ResultAsync.unwrapOr` (method)](#resultasyncunwrapor-method) 55 | - [`ResultAsync.andThen` (method)](#resultasyncandthen-method) 56 | - [`ResultAsync.orElse` (method)](#resultasyncorelse-method) 57 | - [`ResultAsync.match` (method)](#resultasyncmatch-method) 58 | - [`ResultAsync.andTee` (method)](#resultasyncandtee-method) 59 | - [`ResultAsync.orTee` (method)](#resultasyncortee-method) 60 | - [`ResultAsync.andThrough` (method)](#resultasyncandthrough-method) 61 | - [`ResultAsync.combine` (static class method)](#resultasynccombine-static-class-method) 62 | - [`ResultAsync.combineWithAllErrors` (static class method)](#resultasynccombinewithallerrors-static-class-method) 63 | - [`ResultAsync.safeUnwrap()`](#resultasyncsafeunwrap) 64 | + [Utilities](#utilities) 65 | - [`fromThrowable`](#fromthrowable) 66 | - [`fromAsyncThrowable`](#fromasyncthrowable) 67 | - [`fromPromise`](#frompromise) 68 | - [`fromSafePromise`](#fromsafepromise) 69 | - [`safeTry`](#safetry) 70 | + [Testing](#testing) 71 | * [A note on the Package Name](#a-note-on-the-package-name) 72 | 73 | ## Installation 74 | 75 | ```sh 76 | > npm install neverthrow 77 | ``` 78 | 79 | ## Recommended: Use `eslint-plugin-neverthrow` 80 | 81 | As part of `neverthrow`s [bounty program](https://github.com/supermacro/neverthrow/issues/314), user [mdbetancourt](https://github.com/mdbetancourt) created [`eslint-plugin-neverthrow`](https://github.com/mdbetancourt/eslint-plugin-neverthrow) to ensure that errors are not gone unhandled. 82 | 83 | Install by running: 84 | 85 | ```sh 86 | > npm install eslint-plugin-neverthrow 87 | ``` 88 | 89 | With `eslint-plugin-neverthrow`, you are forced to consume the result in one of the following three ways: 90 | 91 | - Calling `.match` 92 | - Calling `.unwrapOr` 93 | - Calling `._unsafeUnwrap` 94 | 95 | This ensures that you're explicitly handling the error of your `Result`. 96 | 97 | This plugin is essentially a porting of Rust's [`must-use`](https://doc.rust-lang.org/std/result/#results-must-be-used) attribute. 98 | 99 | 100 | ## Top-Level API 101 | 102 | `neverthrow` exposes the following: 103 | 104 | - `ok` convenience function to create an `Ok` variant of `Result` 105 | - `err` convenience function to create an `Err` variant of `Result` 106 | - `Ok` class and type 107 | - `Err` class and type 108 | - `Result` Type as well as namespace / object from which to call [`Result.fromThrowable`](#resultfromthrowable-static-class-method), [Result.combine](#resultcombine-static-class-method). 109 | - `ResultAsync` class 110 | - `okAsync` convenience function to create a `ResultAsync` containing an `Ok` type `Result` 111 | - `errAsync` convenience function to create a `ResultAsync` containing an `Err` type `Result` 112 | 113 | ```typescript 114 | import { 115 | ok, 116 | Ok, 117 | err, 118 | Err, 119 | Result, 120 | okAsync, 121 | errAsync, 122 | ResultAsync, 123 | fromAsyncThrowable, 124 | fromThrowable, 125 | fromPromise, 126 | fromSafePromise, 127 | safeTry, 128 | } from 'neverthrow' 129 | ``` 130 | 131 | --- 132 | 133 | **Check out the [wiki](https://github.com/supermacro/neverthrow/wiki) for help on how to make the most of `neverthrow`.** 134 | 135 | If you find this package useful, please consider [sponsoring me](https://github.com/sponsors/supermacro/) or simply [buying me a coffee](https://ko-fi.com/gdelgado)! 136 | 137 | --- 138 | 139 | ## API Documentation 140 | 141 | ### Synchronous API (`Result`) 142 | 143 | #### `ok` 144 | 145 | Constructs an `Ok` variant of `Result` 146 | 147 | **Signature:** 148 | 149 | ```typescript 150 | ok(value: T): Ok { ... } 151 | ``` 152 | 153 | **Example:** 154 | 155 | ```typescript 156 | import { ok } from 'neverthrow' 157 | 158 | const myResult = ok({ myData: 'test' }) // instance of `Ok` 159 | 160 | myResult.isOk() // true 161 | myResult.isErr() // false 162 | ``` 163 | 164 | [⬆️ Back to top](#toc) 165 | 166 | --- 167 | 168 | #### `err` 169 | 170 | Constructs an `Err` variant of `Result` 171 | 172 | **Signature:** 173 | 174 | ```typescript 175 | err(error: E): Err { ... } 176 | ``` 177 | 178 | **Example:** 179 | 180 | ```typescript 181 | import { err } from 'neverthrow' 182 | 183 | const myResult = err('Oh noooo') // instance of `Err` 184 | 185 | myResult.isOk() // false 186 | myResult.isErr() // true 187 | ``` 188 | 189 | [⬆️ Back to top](#toc) 190 | 191 | --- 192 | 193 | #### `Result.isOk` (method) 194 | 195 | Returns `true` if the result is an `Ok` variant 196 | 197 | **Signature:** 198 | 199 | ```typescript 200 | isOk(): boolean { ... } 201 | ``` 202 | 203 | [⬆️ Back to top](#toc) 204 | 205 | --- 206 | 207 | #### `Result.isErr` (method) 208 | 209 | Returns `true` if the result is an `Err` variant 210 | 211 | **Signature**: 212 | 213 | ```typescript 214 | isErr(): boolean { ... } 215 | ``` 216 | 217 | [⬆️ Back to top](#toc) 218 | 219 | --- 220 | 221 | #### `Result.map` (method) 222 | 223 | Maps a `Result` to `Result` by applying a function to a contained `Ok` value, leaving an `Err` value untouched. 224 | 225 | This function can be used to compose the results of two functions. 226 | 227 | **Signature:** 228 | 229 | ```typescript 230 | class Result { 231 | map(callback: (value: T) => U): Result { ... } 232 | } 233 | ``` 234 | 235 | **Example**: 236 | 237 | ```typescript 238 | import { getLines } from 'imaginary-parser' 239 | // ^ assume getLines has the following signature: 240 | // getLines(str: string): Result, Error> 241 | 242 | // since the formatting is deemed correct by `getLines` 243 | // then it means that `linesResult` is an Ok 244 | // containing an Array of strings for each line of code 245 | const linesResult = getLines('1\n2\n3\n4\n') 246 | 247 | // this Result now has a Array inside it 248 | const newResult = linesResult.map( 249 | (arr: Array) => arr.map(parseInt) 250 | ) 251 | 252 | newResult.isOk() // true 253 | ``` 254 | 255 | [⬆️ Back to top](#toc) 256 | 257 | --- 258 | 259 | #### `Result.mapErr` (method) 260 | 261 | Maps a `Result` to `Result` by applying a function to a contained `Err` value, leaving an `Ok` value untouched. 262 | 263 | This function can be used to pass through a successful result while handling an error. 264 | 265 | **Signature:** 266 | 267 | ```typescript 268 | class Result { 269 | mapErr(callback: (error: E) => F): Result { ... } 270 | } 271 | ``` 272 | 273 | **Example**: 274 | 275 | ```typescript 276 | import { parseHeaders } from 'imaginary-http-parser' 277 | // imagine that parseHeaders has the following signature: 278 | // parseHeaders(raw: string): Result 279 | 280 | const rawHeaders = 'nonsensical gibberish and badly formatted stuff' 281 | 282 | const parseResult = parseHeaders(rawHeaders) 283 | 284 | parseResult.mapErr(parseError => { 285 | res.status(400).json({ 286 | error: parseError 287 | }) 288 | }) 289 | 290 | parseResult.isErr() // true 291 | ``` 292 | 293 | [⬆️ Back to top](#toc) 294 | 295 | --- 296 | 297 | #### `Result.unwrapOr` (method) 298 | 299 | Unwrap the `Ok` value, or return the default if there is an `Err` 300 | 301 | **Signature:** 302 | 303 | ```typescript 304 | class Result { 305 | unwrapOr(value: T): T { ... } 306 | } 307 | ``` 308 | 309 | **Example**: 310 | 311 | ```typescript 312 | const myResult = err('Oh noooo') 313 | 314 | const multiply = (value: number): number => value * 2 315 | 316 | const unwrapped: number = myResult.map(multiply).unwrapOr(10) 317 | ``` 318 | 319 | [⬆️ Back to top](#toc) 320 | 321 | --- 322 | 323 | #### `Result.andThen` (method) 324 | 325 | Same idea as `map` above. Except you must return a new `Result`. 326 | 327 | The returned value will be a `Result`. As of `v4.1.0-beta`, you are able to return distinct error types (see signature below). Prior to `v4.1.0-beta`, the error type could not be distinct. 328 | 329 | This is useful for when you need to do a subsequent computation using the inner `T` value, but that computation might fail. 330 | 331 | Additionally, `andThen` is really useful as a tool to flatten a `Result, E1>` into a `Result` (see example below). 332 | 333 | **Signature:** 334 | 335 | ```typescript 336 | class Result { 337 | // Note that the latest version lets you return distinct errors as well. 338 | // If the error types (E and F) are the same (like `string | string`) 339 | // then they will be merged into one type (`string`) 340 | andThen( 341 | callback: (value: T) => Result 342 | ): Result { ... } 343 | } 344 | ``` 345 | 346 | **Example 1: Chaining Results** 347 | 348 | ```typescript 349 | import { err, ok } from 'neverthrow' 350 | 351 | const sq = (n: number): Result => ok(n ** 2) 352 | 353 | ok(2) 354 | .andThen(sq) 355 | .andThen(sq) // Ok(16) 356 | 357 | ok(2) 358 | .andThen(sq) 359 | .andThen(err) // Err(4) 360 | 361 | ok(2) 362 | .andThen(err) 363 | .andThen(sq) // Err(2) 364 | 365 | err(3) 366 | .andThen(sq) 367 | .andThen(sq) // Err(3) 368 | ``` 369 | 370 | **Example 2: Flattening Nested Results** 371 | 372 | ```typescript 373 | // It's common to have nested Results 374 | const nested = ok(ok(1234)) 375 | 376 | // notNested is a Ok(1234) 377 | const notNested = nested.andThen((innerResult) => innerResult) 378 | ``` 379 | 380 | [⬆️ Back to top](#toc) 381 | 382 | --- 383 | 384 | #### `Result.asyncAndThen` (method) 385 | 386 | Same idea as [`andThen` above](#resultandthen-method), except you must return a new `ResultAsync`. 387 | 388 | The returned value will be a `ResultAsync`. 389 | 390 | **Signature:** 391 | 392 | ```typescript 393 | class Result { 394 | asyncAndThen( 395 | callback: (value: T) => ResultAsync 396 | ): ResultAsync { ... } 397 | } 398 | ``` 399 | 400 | [⬆️ Back to top](#toc) 401 | 402 | --- 403 | 404 | #### `Result.orElse` (method) 405 | 406 | Takes an `Err` value and maps it to a `Result`. This is useful for error recovery. 407 | 408 | **Signature:** 409 | 410 | ```typescript 411 | class Result { 412 | orElse( 413 | callback: (error: E) => Result 414 | ): Result { ... } 415 | } 416 | ``` 417 | 418 | **Example:** 419 | 420 | ```typescript 421 | enum DatabaseError { 422 | PoolExhausted = 'PoolExhausted', 423 | NotFound = 'NotFound', 424 | } 425 | 426 | const dbQueryResult: Result = err(DatabaseError.NotFound) 427 | 428 | const updatedQueryResult = dbQueryResult.orElse((dbError) => 429 | dbError === DatabaseError.NotFound 430 | ? ok('User does not exist') // error recovery branch: ok() must be called with a value of type string 431 | // 432 | // 433 | // err() can be called with a value of any new type that you want 434 | // it could also be called with the same error value 435 | // 436 | // err(dbError) 437 | : err(500) 438 | ) 439 | ``` 440 | 441 | [⬆️ Back to top](#toc) 442 | 443 | --- 444 | 445 | #### `Result.match` (method) 446 | 447 | Given 2 functions (one for the `Ok` variant and one for the `Err` variant) execute the function that matches the `Result` variant. 448 | 449 | Match callbacks do not necessitate to return a `Result`, however you can return a `Result` if you want to. 450 | 451 | **Signature:** 452 | 453 | ```typescript 454 | class Result { 455 | match( 456 | okCallback: (value: T) => A, 457 | errorCallback: (error: E) => B 458 | ): A | B => { ... } 459 | } 460 | ``` 461 | 462 | `match` is like chaining `map` and `mapErr`, with the distinction that with `match` both functions must have the same return type. 463 | The differences between `match` and chaining `map` and `mapErr` are that: 464 | - with `match` both functions must have the same return type `A` 465 | - `match` unwraps the `Result` into an `A` (the match functions' return type) 466 | - This makes no difference if you are performing side effects only 467 | 468 | **Example:** 469 | 470 | ```typescript 471 | // map/mapErr api 472 | // note that you DON'T have to append mapErr 473 | // after map which means that you are not required to do 474 | // error handling 475 | computationThatMightFail().map(console.log).mapErr(console.error) 476 | 477 | // match api 478 | // works exactly the same as above since both callbacks 479 | // only perform side effects, 480 | // except, now you HAVE to do error handling :) 481 | computationThatMightFail().match(console.log, console.error) 482 | 483 | // Returning values 484 | const attempt = computationThatMightFail() 485 | .map((str) => str.toUpperCase()) 486 | .mapErr((err) => `Error: ${err}`) 487 | // `attempt` is of type `Result` 488 | 489 | const answer = computationThatMightFail().match( 490 | (str) => str.toUpperCase(), 491 | (err) => `Error: ${err}` 492 | ) 493 | // `answer` is of type `string` 494 | ``` 495 | 496 | If you don't use the error parameter in your match callback then `match` is equivalent to chaining `map` with `unwrapOr`: 497 | ```ts 498 | const answer = computationThatMightFail().match( 499 | (str) => str.toUpperCase(), 500 | () => 'ComputationError' 501 | ) 502 | // `answer` is of type `string` 503 | 504 | const answer = computationThatMightFail() 505 | .map((str) => str.toUpperCase()) 506 | .unwrapOr('ComputationError') 507 | ``` 508 | 509 | 510 | [⬆️ Back to top](#toc) 511 | 512 | --- 513 | 514 | #### `Result.asyncMap` (method) 515 | 516 | Similar to `map` except for two things: 517 | 518 | - the mapping function must return a `Promise` 519 | - asyncMap returns a `ResultAsync` 520 | 521 | You can then chain the result of `asyncMap` using the `ResultAsync` apis (like `map`, `mapErr`, `andThen`, etc.) 522 | 523 | **Signature:** 524 | 525 | ```typescript 526 | class Result { 527 | asyncMap( 528 | callback: (value: T) => Promise 529 | ): ResultAsync { ... } 530 | } 531 | ``` 532 | 533 | **Example:** 534 | 535 | ```typescript 536 | import { parseHeaders } from 'imaginary-http-parser' 537 | // imagine that parseHeaders has the following signature: 538 | // parseHeaders(raw: string): Result 539 | 540 | const asyncRes = parseHeaders(rawHeader) 541 | .map(headerKvMap => headerKvMap.Authorization) 542 | .asyncMap(findUserInDatabase) 543 | ``` 544 | 545 | Note that in the above example if `parseHeaders` returns an `Err` then `.map` and `.asyncMap` will not be invoked, and `asyncRes` variable will resolve to an `Err` when turned into a `Result` using `await` or `.then()`. 546 | 547 | [⬆️ Back to top](#toc) 548 | 549 | --- 550 | 551 | #### `Result.andTee` (method) 552 | 553 | Takes a `Result` and lets the original `Result` pass through regardless the result of the passed-in function. 554 | This is a handy way to handle side effects whose failure or success should not affect your main logics such as logging. 555 | 556 | **Signature:** 557 | 558 | ```typescript 559 | class Result { 560 | andTee( 561 | callback: (value: T) => unknown 562 | ): Result { ... } 563 | } 564 | ``` 565 | 566 | **Example:** 567 | 568 | ```typescript 569 | import { parseUserInput } from 'imaginary-parser' 570 | import { logUser } from 'imaginary-logger' 571 | import { insertUser } from 'imaginary-database' 572 | 573 | // ^ assume parseUserInput, logUser and insertUser have the following signatures: 574 | // parseUserInput(input: RequestData): Result 575 | // logUser(user: User): Result 576 | // insertUser(user: User): ResultAsync 577 | // Note logUser returns void upon success but insertUser takes User type. 578 | 579 | const resAsync = parseUserInput(userInput) 580 | .andTee(logUser) 581 | .asyncAndThen(insertUser) 582 | 583 | // Note no LogError shows up in the Result type 584 | resAsync.then((res: Result) => { 585 | if(res.isErr()){ 586 | console.log("Oops, at least one step failed", res.error) 587 | } 588 | else{ 589 | console.log("User input has been parsed and inserted successfully.") 590 | } 591 | }) 592 | ``` 593 | 594 | [⬆️ Back to top](#toc) 595 | 596 | --- 597 | 598 | #### `Result.orTee` (method) 599 | 600 | Like `andTee` for the error track. Takes a `Result` and lets the `Err` value pass through regardless the result of the passed-in function. 601 | This is a handy way to handle side effects whose failure or success should not affect your main logics such as logging. 602 | 603 | **Signature:** 604 | 605 | ```typescript 606 | class Result { 607 | orTee( 608 | callback: (value: E) => unknown 609 | ): Result { ... } 610 | } 611 | ``` 612 | 613 | **Example:** 614 | 615 | ```typescript 616 | import { parseUserInput } from 'imaginary-parser' 617 | import { logParseError } from 'imaginary-logger' 618 | import { insertUser } from 'imaginary-database' 619 | 620 | // ^ assume parseUserInput, logParseError and insertUser have the following signatures: 621 | // parseUserInput(input: RequestData): Result 622 | // logParseError(parseError: ParseError): Result 623 | // insertUser(user: User): ResultAsync 624 | // Note logParseError returns void upon success but insertUser takes User type. 625 | 626 | const resAsync = parseUserInput(userInput) 627 | .orTee(logParseError) 628 | .asyncAndThen(insertUser) 629 | 630 | // Note no LogError shows up in the Result type 631 | resAsync.then((res: Result) => { 632 | if(res.isErr()){ 633 | console.log("Oops, at least one step failed", res.error) 634 | } 635 | else{ 636 | console.log("User input has been parsed and inserted successfully.") 637 | } 638 | }) 639 | ``` 640 | 641 | [⬆️ Back to top](#toc) 642 | 643 | --- 644 | 645 | #### `Result.andThrough` (method) 646 | 647 | Similar to `andTee` except for: 648 | 649 | - when there is an error from the passed-in function, that error will be passed along. 650 | 651 | **Signature:** 652 | 653 | ```typescript 654 | class Result { 655 | andThrough( 656 | callback: (value: T) => Result 657 | ): Result { ... } 658 | } 659 | ``` 660 | 661 | **Example:** 662 | 663 | ```typescript 664 | import { parseUserInput } from 'imaginary-parser' 665 | import { validateUser } from 'imaginary-validator' 666 | import { insertUser } from 'imaginary-database' 667 | 668 | // ^ assume parseUseInput, validateUser and insertUser have the following signatures: 669 | // parseUserInput(input: RequestData): Result 670 | // validateUser(user: User): Result 671 | // insertUser(user: User): ResultAsync 672 | // Note validateUser returns void upon success but insertUser takes User type. 673 | 674 | const resAsync = parseUserInput(userInput) 675 | .andThrough(validateUser) 676 | .asyncAndThen(insertUser) 677 | 678 | resAsync.then((res: Result) => { 679 | if(res.isErr()){ 680 | console.log("Oops, at least one step failed", res.error) 681 | } 682 | else{ 683 | console.log("User input has been parsed, validated, inserted successfully.") 684 | } 685 | }) 686 | ``` 687 | 688 | [⬆️ Back to top](#toc) 689 | 690 | --- 691 | 692 | #### `Result.asyncAndThrough` (method) 693 | 694 | Similar to `andThrough` except you must return a ResultAsync. 695 | 696 | You can then chain the result of `asyncAndThrough` using the `ResultAsync` apis (like `map`, `mapErr`, `andThen`, etc.) 697 | 698 | **Signature:** 699 | 700 | ```typescript 701 | import { parseUserInput } from 'imaginary-parser' 702 | import { insertUser } from 'imaginary-database' 703 | import { sendNotification } from 'imaginary-service' 704 | 705 | // ^ assume parseUserInput, insertUser and sendNotification have the following signatures: 706 | // parseUserInput(input: RequestData): Result 707 | // insertUser(user: User): ResultAsync 708 | // sendNotification(user: User): ResultAsync 709 | // Note insertUser returns void upon success but sendNotification takes User type. 710 | 711 | const resAsync = parseUserInput(userInput) 712 | .asyncAndThrough(insertUser) 713 | .andThen(sendNotification) 714 | 715 | resAsync.then((res: Result) => { 716 | if(res.isErr()){ 717 | console.log("Oops, at least one step failed", res.error) 718 | } 719 | else{ 720 | console.log("User has been parsed, inserted and notified successfully.") 721 | } 722 | }) 723 | ``` 724 | 725 | [⬆️ Back to top](#toc) 726 | 727 | --- 728 | #### `Result.fromThrowable` (static class method) 729 | 730 | > Although Result is not an actual JS class, the way that `fromThrowable` has been implemented requires that you call `fromThrowable` as though it were a static method on `Result`. See examples below. 731 | 732 | The JavaScript community has agreed on the convention of throwing exceptions. 733 | As such, when interfacing with third party libraries it's imperative that you 734 | wrap third-party code in try / catch blocks. 735 | 736 | This function will create a new function that returns an `Err` when the original 737 | function throws. 738 | 739 | It is not possible to know the types of the errors thrown in the original 740 | function, therefore it is recommended to use the second argument `errorFn` to 741 | map what is thrown to a known type. 742 | 743 | **Example**: 744 | 745 | ```typescript 746 | import { Result } from 'neverthrow' 747 | 748 | type ParseError = { message: string } 749 | const toParseError = (): ParseError => ({ message: "Parse Error" }) 750 | 751 | const safeJsonParse = Result.fromThrowable(JSON.parse, toParseError) 752 | 753 | // the function can now be used safely, if the function throws, the result will be an Err 754 | const res = safeJsonParse("{"); 755 | ``` 756 | 757 | [⬆️ Back to top](#toc) 758 | 759 | --- 760 | 761 | #### `Result.combine` (static class method) 762 | 763 | > Although Result is not an actual JS class, the way that `combine` has been implemented requires that you call `combine` as though it were a static method on `Result`. See examples below. 764 | 765 | Combine lists of `Result`s. 766 | 767 | If you're familiar with `Promise.all`, the combine function works conceptually the same. 768 | 769 | **`combine` works on both heterogeneous and homogeneous lists**. This means that you can have lists that contain different kinds of `Result`s and still be able to combine them. Note that you cannot combine lists that contain both `Result`s **and** `ResultAsync`s. 770 | 771 | The combine function takes a list of results and returns a single result. If all the results in the list are `Ok`, then the return value will be a `Ok` containing a list of all the individual `Ok` values. 772 | 773 | If just one of the results in the list is an `Err` then the combine function returns that Err value (it short circuits and returns the first Err that it finds). 774 | 775 | Formally speaking: 776 | 777 | ```typescript 778 | // homogeneous lists 779 | function combine(resultList: Result[]): Result 780 | 781 | // heterogeneous lists 782 | function combine(resultList: [ Result, Result ]): Result<[ T1, T2 ], E1 | E2> 783 | function combine => Result<[ T1, T2, T3 ], E1 | E2 | E3> 784 | function combine => Result<[ T1, T2, T3, T4 ], E1 | E2 | E3 | E4> 785 | // ... etc etc ad infinitum 786 | 787 | ``` 788 | 789 | Example: 790 | ```typescript 791 | const resultList: Result[] = 792 | [ok(1), ok(2)] 793 | 794 | const combinedList: Result = 795 | Result.combine(resultList) 796 | ``` 797 | 798 | Example with tuples: 799 | ```typescript 800 | /** @example tuple(1, 2, 3) === [1, 2, 3] // with type [number, number, number] */ 801 | const tuple = (...args: T): T => args 802 | 803 | const resultTuple: [Result, Result] = 804 | tuple(ok('a'), ok('b')) 805 | 806 | const combinedTuple: Result<[string, string], unknown> = 807 | Result.combine(resultTuple) 808 | ``` 809 | 810 | [⬆️ Back to top](#toc) 811 | 812 | --- 813 | 814 | #### `Result.combineWithAllErrors` (static class method) 815 | 816 | > Although Result is not an actual JS class, the way that `combineWithAllErrors` has been implemented requires that you call `combineWithAllErrors` as though it were a static method on `Result`. See examples below. 817 | 818 | Like `combine` but without short-circuiting. Instead of just the first error value, you get a list of all error values of the input result list. 819 | 820 | If only some results fail, the new combined error list will only contain the error value of the failed results, meaning that there is no guarantee of the length of the new error list. 821 | 822 | Function signature: 823 | 824 | ```typescript 825 | // homogeneous lists 826 | function combineWithAllErrors(resultList: Result[]): Result 827 | 828 | // heterogeneous lists 829 | function combineWithAllErrors(resultList: [ Result, Result ]): Result<[ T1, T2 ], (E1 | E2)[]> 830 | function combineWithAllErrors => Result<[ T1, T2, T3 ], (E1 | E2 | E3)[]> 831 | function combineWithAllErrors => Result<[ T1, T2, T3, T4 ], (E1 | E2 | E3 | E4)[]> 832 | // ... etc etc ad infinitum 833 | ``` 834 | 835 | Example usage: 836 | 837 | ```typescript 838 | const resultList: Result[] = [ 839 | ok(123), 840 | err('boooom!'), 841 | ok(456), 842 | err('ahhhhh!'), 843 | ] 844 | 845 | const result = Result.combineWithAllErrors(resultList) 846 | 847 | // result is Err(['boooom!', 'ahhhhh!']) 848 | ``` 849 | 850 | [⬆️ Back to top](#toc) 851 | 852 | #### `Result.safeUnwrap()` 853 | 854 | **Deprecated**. You don't need to use this method anymore. 855 | 856 | Allows for unwrapping a `Result` or returning an `Err` implicitly, thereby reducing boilerplate. 857 | 858 | 859 | [⬆️ Back to top](#toc) 860 | 861 | --- 862 | 863 | ### Asynchronous API (`ResultAsync`) 864 | 865 | #### `okAsync` 866 | 867 | Constructs an `Ok` variant of `ResultAsync` 868 | 869 | **Signature:** 870 | 871 | ```typescript 872 | okAsync(value: T): ResultAsync 873 | ``` 874 | 875 | **Example:** 876 | 877 | ```typescript 878 | import { okAsync } from 'neverthrow' 879 | 880 | const myResultAsync = okAsync({ myData: 'test' }) // instance of `ResultAsync` 881 | 882 | const myResult = await myResultAsync // instance of `Ok` 883 | 884 | myResult.isOk() // true 885 | myResult.isErr() // false 886 | ``` 887 | 888 | [⬆️ Back to top](#toc) 889 | 890 | --- 891 | 892 | #### `errAsync` 893 | 894 | Constructs an `Err` variant of `ResultAsync` 895 | 896 | **Signature:** 897 | 898 | ```typescript 899 | errAsync(error: E): ResultAsync 900 | ``` 901 | 902 | **Example:** 903 | 904 | ```typescript 905 | import { errAsync } from 'neverthrow' 906 | 907 | const myResultAsync = errAsync('Oh nooo') // instance of `ResultAsync` 908 | 909 | const myResult = await myResultAsync // instance of `Err` 910 | 911 | myResult.isOk() // false 912 | myResult.isErr() // true 913 | ``` 914 | 915 | [⬆️ Back to top](#toc) 916 | 917 | --- 918 | 919 | #### `ResultAsync.fromThrowable` (static class method) 920 | 921 | Similar to [Result.fromThrowable](#resultfromthrowable-static-class-method), but for functions that return a `Promise`. 922 | 923 | **Example**: 924 | 925 | ```typescript 926 | import { ResultAsync } from 'neverthrow' 927 | import { insertIntoDb } from 'imaginary-database' 928 | // insertIntoDb(user: User): Promise 929 | 930 | const insertUser = ResultAsync.fromThrowable(insertIntoDb, () => new Error('Database error')) 931 | // `res` has a type of (user: User) => ResultAsync 932 | ``` 933 | 934 | Note that this can be safer than using [ResultAsync.fromPromise](#resultasyncfrompromise-static-class-method) with 935 | the result of a function call, because not all functions that return a `Promise` are `async`, and thus they can throw 936 | errors synchronously rather than returning a rejected `Promise`. For example: 937 | 938 | ```typescript 939 | // NOT SAFE !! 940 | import { ResultAsync } from 'neverthrow' 941 | import { db } from 'imaginary-database' 942 | // db.insert(table: string, value: T): Promise 943 | 944 | const insertUser = (user: User): Promise => { 945 | if (!user.id) { 946 | // this throws synchronously! 947 | throw new TypeError('missing user id') 948 | } 949 | return db.insert('users', user) 950 | } 951 | 952 | // this will throw, NOT return a `ResultAsync` 953 | const res = ResultAsync.fromPromise(insertIntoDb(myUser), () => new Error('Database error')) 954 | ``` 955 | 956 | [⬆️ Back to top](#toc) 957 | 958 | --- 959 | 960 | #### `ResultAsync.fromPromise` (static class method) 961 | 962 | Transforms a `PromiseLike` (that may throw) into a `ResultAsync`. 963 | 964 | The second argument handles the rejection case of the promise and maps the error from `unknown` into some type `E`. 965 | 966 | 967 | **Signature:** 968 | 969 | ```typescript 970 | // fromPromise is a static class method 971 | // also available as a standalone function 972 | // import { fromPromise } from 'neverthrow' 973 | ResultAsync.fromPromise( 974 | promise: PromiseLike, 975 | errorHandler: (unknownError: unknown) => E) 976 | ): ResultAsync { ... } 977 | ``` 978 | 979 | If you are working with `PromiseLike` objects that you **know for a fact** will not throw, then use `fromSafePromise` in order to avoid having to pass a redundant `errorHandler` argument. 980 | 981 | **Example**: 982 | 983 | ```typescript 984 | import { ResultAsync } from 'neverthrow' 985 | import { insertIntoDb } from 'imaginary-database' 986 | // insertIntoDb(user: User): Promise 987 | 988 | const res = ResultAsync.fromPromise(insertIntoDb(myUser), () => new Error('Database error')) 989 | // `res` has a type of ResultAsync 990 | ``` 991 | 992 | [⬆️ Back to top](#toc) 993 | 994 | --- 995 | 996 | #### `ResultAsync.fromSafePromise` (static class method) 997 | 998 | Same as `ResultAsync.fromPromise` except that it does not handle the rejection of the promise. **Ensure you know what you're doing, otherwise a thrown exception within this promise will cause ResultAsync to reject, instead of resolve to a Result.** 999 | 1000 | **Signature:** 1001 | 1002 | ```typescript 1003 | // fromPromise is a static class method 1004 | // also available as a standalone function 1005 | // import { fromPromise } from 'neverthrow' 1006 | ResultAsync.fromSafePromise( 1007 | promise: PromiseLike 1008 | ): ResultAsync { ... } 1009 | ``` 1010 | 1011 | **Example**: 1012 | 1013 | ```typescript 1014 | import { RouteError } from 'routes/error' 1015 | 1016 | // simulate slow routes in an http server that works in a Result / ResultAsync context 1017 | // Adopted from https://github.com/parlez-vous/server/blob/2496bacf55a2acbebc30631b5562f34272794d76/src/routes/common/signup.ts 1018 | export const slowDown = (ms: number) => (value: T) => 1019 | ResultAsync.fromSafePromise( 1020 | new Promise((resolve) => { 1021 | setTimeout(() => { 1022 | resolve(value) 1023 | }, ms) 1024 | }) 1025 | ) 1026 | 1027 | export const signupHandler = route((req, sessionManager) => 1028 | decode(userSignupDecoder, req.body, 'Invalid request body').map((parsed) => { 1029 | return createUser(parsed) 1030 | .andThen(slowDown(3000)) // slowdown by 3 seconds 1031 | .andThen(sessionManager.createSession) 1032 | .map(({ sessionToken, admin }) => AppData.init(admin, sessionToken)) 1033 | }) 1034 | ) 1035 | ``` 1036 | 1037 | [⬆️ Back to top](#toc) 1038 | 1039 | --- 1040 | 1041 | #### `ResultAsync.map` (method) 1042 | 1043 | Maps a `ResultAsync` to `ResultAsync` by applying a function to a contained `Ok` value, leaving an `Err` value untouched. 1044 | 1045 | The applied function can be synchronous or asynchronous (returning a `Promise`) with no impact to the return type. 1046 | 1047 | This function can be used to compose the results of two functions. 1048 | 1049 | **Signature:** 1050 | 1051 | ```typescript 1052 | class ResultAsync { 1053 | map( 1054 | callback: (value: T) => U | Promise 1055 | ): ResultAsync { ... } 1056 | } 1057 | ``` 1058 | 1059 | **Example**: 1060 | 1061 | ```typescript 1062 | import { findUsersIn } from 'imaginary-database' 1063 | // ^ assume findUsersIn has the following signature: 1064 | // findUsersIn(country: string): ResultAsync, Error> 1065 | 1066 | const usersInCanada = findUsersIn("Canada") 1067 | 1068 | // Let's assume we only need their names 1069 | const namesInCanada = usersInCanada.map((users: Array) => users.map(user => user.name)) 1070 | // namesInCanada is of type ResultAsync, Error> 1071 | 1072 | // We can extract the Result using .then() or await 1073 | namesInCanada.then((namesResult: Result, Error>) => { 1074 | if(namesResult.isErr()){ 1075 | console.log("Couldn't get the users from the database", namesResult.error) 1076 | } 1077 | else{ 1078 | console.log("Users in Canada are named: " + namesResult.value.join(',')) 1079 | } 1080 | }) 1081 | ``` 1082 | 1083 | [⬆️ Back to top](#toc) 1084 | 1085 | --- 1086 | 1087 | #### `ResultAsync.mapErr` (method) 1088 | 1089 | Maps a `ResultAsync` to `ResultAsync` by applying a function to a contained `Err` value, leaving an `Ok` value untouched. 1090 | 1091 | The applied function can be synchronous or asynchronous (returning a `Promise`) with no impact to the return type. 1092 | 1093 | This function can be used to pass through a successful result while handling an error. 1094 | 1095 | **Signature:** 1096 | 1097 | ```typescript 1098 | class ResultAsync { 1099 | mapErr( 1100 | callback: (error: E) => F | Promise 1101 | ): ResultAsync { ... } 1102 | } 1103 | ``` 1104 | 1105 | **Example**: 1106 | 1107 | ```typescript 1108 | import { findUsersIn } from 'imaginary-database' 1109 | // ^ assume findUsersIn has the following signature: 1110 | // findUsersIn(country: string): ResultAsync, Error> 1111 | 1112 | // Let's say we need to low-level errors from findUsersIn to be more readable 1113 | const usersInCanada = findUsersIn("Canada").mapErr((error: Error) => { 1114 | // The only error we want to pass to the user is "Unknown country" 1115 | if(error.message === "Unknown country"){ 1116 | return error.message 1117 | } 1118 | // All other errors will be labelled as a system error 1119 | return "System error, please contact an administrator." 1120 | }) 1121 | 1122 | // usersInCanada is of type ResultAsync, string> 1123 | 1124 | usersInCanada.then((usersResult: Result, string>) => { 1125 | if(usersResult.isErr()){ 1126 | res.status(400).json({ 1127 | error: usersResult.error 1128 | }) 1129 | } 1130 | else{ 1131 | res.status(200).json({ 1132 | users: usersResult.value 1133 | }) 1134 | } 1135 | }) 1136 | ``` 1137 | 1138 | [⬆️ Back to top](#toc) 1139 | 1140 | --- 1141 | 1142 | #### `ResultAsync.unwrapOr` (method) 1143 | 1144 | Unwrap the `Ok` value, or return the default if there is an `Err`. 1145 | Works just like `Result.unwrapOr` but returns a `Promise` instead of `T`. 1146 | 1147 | **Signature:** 1148 | 1149 | ```typescript 1150 | class ResultAsync { 1151 | unwrapOr(value: T): Promise { ... } 1152 | } 1153 | ``` 1154 | 1155 | **Example**: 1156 | 1157 | ```typescript 1158 | const unwrapped: number = await errAsync(0).unwrapOr(10) 1159 | // unwrapped = 10 1160 | ``` 1161 | 1162 | [⬆️ Back to top](#toc) 1163 | 1164 | --- 1165 | 1166 | #### `ResultAsync.andThen` (method) 1167 | 1168 | Same idea as `map` above. Except the applied function must return a `Result` or `ResultAsync`. 1169 | 1170 | `ResultAsync.andThen` always returns a `ResultAsync` no matter the return type of the applied function. 1171 | 1172 | This is useful for when you need to do a subsequent computation using the inner `T` value, but that computation might fail. 1173 | 1174 | `andThen` is really useful as a tool to flatten a `ResultAsync, E1>` into a `ResultAsync` (see example below). 1175 | 1176 | **Signature:** 1177 | 1178 | ```typescript 1179 | // Note that the latest version (v4.1.0-beta) lets you return distinct errors as well. 1180 | // If the error types (E and F) are the same (like `string | string`) 1181 | // then they will be merged into one type (`string`) 1182 | 1183 | class ResultAsync { 1184 | andThen( 1185 | callback: (value: T) => Result | ResultAsync 1186 | ): ResultAsync { ... } 1187 | } 1188 | ``` 1189 | 1190 | **Example** 1191 | 1192 | ```typescript 1193 | 1194 | import { validateUser } from 'imaginary-validator' 1195 | import { insertUser } from 'imaginary-database' 1196 | import { sendNotification } from 'imaginary-service' 1197 | 1198 | // ^ assume validateUser, insertUser and sendNotification have the following signatures: 1199 | // validateUser(user: User): Result 1200 | // insertUser(user): ResultAsync 1201 | // sendNotification(user): ResultAsync 1202 | 1203 | const resAsync = validateUser(user) 1204 | .andThen(insertUser) 1205 | .andThen(sendNotification) 1206 | 1207 | // resAsync is a ResultAsync 1208 | 1209 | resAsync.then((res: Result) => { 1210 | if(res.isErr()){ 1211 | console.log("Oops, at least one step failed", res.error) 1212 | } 1213 | else{ 1214 | console.log("User has been validated, inserted and notified successfully.") 1215 | } 1216 | }) 1217 | ``` 1218 | 1219 | [⬆️ Back to top](#toc) 1220 | 1221 | --- 1222 | 1223 | #### `ResultAsync.orElse` (method) 1224 | 1225 | Takes an `Err` value and maps it to a `ResultAsync`. This is useful for error recovery. 1226 | 1227 | **Signature:** 1228 | 1229 | ```typescript 1230 | class ResultAsync { 1231 | orElse( 1232 | callback: (error: E) => Result | ResultAsync 1233 | ): ResultAsync { ... } 1234 | } 1235 | ``` 1236 | 1237 | [⬆️ Back to top](#toc) 1238 | 1239 | --- 1240 | 1241 | #### `ResultAsync.match` (method) 1242 | 1243 | Given 2 functions (one for the `Ok` variant and one for the `Err` variant) execute the function that matches the `ResultAsync` variant. 1244 | 1245 | The difference with `Result.match` is that it always returns a `Promise` because of the asynchronous nature of the `ResultAsync`. 1246 | 1247 | **Signature:** 1248 | 1249 | ```typescript 1250 | class ResultAsync { 1251 | match( 1252 | okCallback: (value: T) => A, 1253 | errorCallback: (error: E) => B 1254 | ): Promise => { ... } 1255 | } 1256 | ``` 1257 | 1258 | **Example:** 1259 | 1260 | ```typescript 1261 | 1262 | import { validateUser } from 'imaginary-validator' 1263 | import { insertUser } from 'imaginary-database' 1264 | 1265 | // ^ assume validateUser and insertUser have the following signatures: 1266 | // validateUser(user: User): Result 1267 | // insertUser(user): ResultAsync 1268 | 1269 | // Handle both cases at the end of the chain using match 1270 | const resultMessage = await validateUser(user) 1271 | .andThen(insertUser) 1272 | .match( 1273 | (user: User) => `User ${user.name} has been successfully created`, 1274 | (error: Error) => `User could not be created because ${error.message}` 1275 | ) 1276 | 1277 | // resultMessage is a string 1278 | ``` 1279 | 1280 | [⬆️ Back to top](#toc) 1281 | 1282 | --- 1283 | #### `ResultAsync.andTee` (method) 1284 | 1285 | Takes a `ResultAsync` and lets the original `ResultAsync` pass through regardless 1286 | the result of the passed-in function. 1287 | This is a handy way to handle side effects whose failure or success should not affect your main logics such as logging. 1288 | 1289 | **Signature:** 1290 | 1291 | ```typescript 1292 | class ResultAsync { 1293 | andTee( 1294 | callback: (value: T) => unknown 1295 | ): ResultAsync => { ... } 1296 | } 1297 | ``` 1298 | 1299 | **Example:** 1300 | 1301 | ```typescript 1302 | import { insertUser } from 'imaginary-database' 1303 | import { logUser } from 'imaginary-logger' 1304 | import { sendNotification } from 'imaginary-service' 1305 | 1306 | // ^ assume insertUser, logUser and sendNotification have the following signatures: 1307 | // insertUser(user: User): ResultAsync 1308 | // logUser(user: User): Result 1309 | // sendNotification(user: User): ResultAsync 1310 | // Note logUser returns void on success but sendNotification takes User type. 1311 | 1312 | const resAsync = insertUser(user) 1313 | .andTee(logUser) 1314 | .andThen(sendNotification) 1315 | 1316 | // Note there is no LogError in the types below 1317 | resAsync.then((res: Result) => { 1318 | if(res.isErr()){ 1319 | console.log("Oops, at least one step failed", res.error) 1320 | } 1321 | else{ 1322 | console.log("User has been inserted and notified successfully.") 1323 | } 1324 | }) 1325 | ``` 1326 | 1327 | [⬆️ Back to top](#toc) 1328 | 1329 | --- 1330 | #### `ResultAsync.orTee` (method) 1331 | 1332 | Like `andTee` for the error track. Takes a `ResultAsync` and lets the original `Err` value pass through regardless 1333 | the result of the passed-in function. 1334 | This is a handy way to handle side effects whose failure or success should not affect your main logics such as logging. 1335 | 1336 | **Signature:** 1337 | 1338 | ```typescript 1339 | class ResultAsync { 1340 | orTee( 1341 | callback: (value: E) => unknown 1342 | ): ResultAsync => { ... } 1343 | } 1344 | ``` 1345 | 1346 | **Example:** 1347 | 1348 | ```typescript 1349 | import { insertUser } from 'imaginary-database' 1350 | import { logInsertError } from 'imaginary-logger' 1351 | import { sendNotification } from 'imaginary-service' 1352 | 1353 | // ^ assume insertUser, logInsertError and sendNotification have the following signatures: 1354 | // insertUser(user: User): ResultAsync 1355 | // logInsertError(insertError: InsertError): Result 1356 | // sendNotification(user: User): ResultAsync 1357 | // Note logInsertError returns void on success but sendNotification takes User type. 1358 | 1359 | const resAsync = insertUser(user) 1360 | .orTee(logUser) 1361 | .andThen(sendNotification) 1362 | 1363 | // Note there is no LogError in the types below 1364 | resAsync.then((res: Result) => { 1365 | if(res.isErr()){ 1366 | console.log("Oops, at least one step failed", res.error) 1367 | } 1368 | else{ 1369 | console.log("User has been inserted and notified successfully.") 1370 | } 1371 | }) 1372 | ``` 1373 | 1374 | [⬆️ Back to top](#toc) 1375 | 1376 | --- 1377 | #### `ResultAsync.andThrough` (method) 1378 | 1379 | 1380 | Similar to `andTee` except for: 1381 | 1382 | - when there is an error from the passed-in function, that error will be passed along. 1383 | 1384 | **Signature:** 1385 | 1386 | ```typescript 1387 | class ResultAsync { 1388 | andThrough( 1389 | callback: (value: T) => Result | ResultAsync, 1390 | ): ResultAsync => { ... } 1391 | } 1392 | ``` 1393 | 1394 | **Example:** 1395 | 1396 | ```typescript 1397 | 1398 | import { buildUser } from 'imaginary-builder' 1399 | import { insertUser } from 'imaginary-database' 1400 | import { sendNotification } from 'imaginary-service' 1401 | 1402 | // ^ assume buildUser, insertUser and sendNotification have the following signatures: 1403 | // buildUser(userRaw: UserRaw): ResultAsync 1404 | // insertUser(user: User): ResultAsync 1405 | // sendNotification(user: User): ResultAsync 1406 | // Note insertUser returns void upon success but sendNotification takes User type. 1407 | 1408 | const resAsync = buildUser(userRaw) 1409 | .andThrough(insertUser) 1410 | .andThen(sendNotification) 1411 | 1412 | resAsync.then((res: Result) => { 1413 | if(res.isErr()){ 1414 | console.log("Oops, at least one step failed", res.error) 1415 | } 1416 | else{ 1417 | console.log("User data has been built, inserted and notified successfully.") 1418 | } 1419 | }) 1420 | ``` 1421 | 1422 | [⬆️ Back to top](#toc) 1423 | 1424 | --- 1425 | #### `ResultAsync.combine` (static class method) 1426 | 1427 | Combine lists of `ResultAsync`s. 1428 | 1429 | If you're familiar with `Promise.all`, the combine function works conceptually the same. 1430 | 1431 | **`combine` works on both heterogeneous and homogeneous lists**. This means that you can have lists that contain different kinds of `ResultAsync`s and still be able to combine them. Note that you cannot combine lists that contain both `Result`s **and** `ResultAsync`s. 1432 | 1433 | The combine function takes a list of results and returns a single result. If all the results in the list are `Ok`, then the return value will be a `Ok` containing a list of all the individual `Ok` values. 1434 | 1435 | If just one of the results in the list is an `Err` then the combine function returns that Err value (it short circuits and returns the first Err that it finds). 1436 | 1437 | Formally speaking: 1438 | 1439 | ```typescript 1440 | // homogeneous lists 1441 | function combine(resultList: ResultAsync[]): ResultAsync 1442 | 1443 | // heterogeneous lists 1444 | function combine(resultList: [ ResultAsync, ResultAsync ]): ResultAsync<[ T1, T2 ], E1 | E2> 1445 | function combine => ResultAsync<[ T1, T2, T3 ], E1 | E2 | E3> 1446 | function combine => ResultAsync<[ T1, T2, T3, T4 ], E1 | E2 | E3 | E4> 1447 | // ... etc etc ad infinitum 1448 | 1449 | ``` 1450 | 1451 | Example: 1452 | ```typescript 1453 | const resultList: ResultAsync[] = 1454 | [okAsync(1), okAsync(2)] 1455 | 1456 | const combinedList: ResultAsync = 1457 | ResultAsync.combine(resultList) 1458 | ``` 1459 | 1460 | Example with tuples: 1461 | ```typescript 1462 | /** @example tuple(1, 2, 3) === [1, 2, 3] // with type [number, number, number] */ 1463 | const tuple = (...args: T): T => args 1464 | 1465 | const resultTuple: [ResultAsync, ResultAsync] = 1466 | tuple(okAsync('a'), okAsync('b')) 1467 | 1468 | const combinedTuple: ResultAsync<[string, string], unknown> = 1469 | ResultAsync.combine(resultTuple) 1470 | ``` 1471 | [⬆️ Back to top](#toc) 1472 | 1473 | --- 1474 | 1475 | #### `ResultAsync.combineWithAllErrors` (static class method) 1476 | 1477 | Like `combine` but without short-circuiting. Instead of just the first error value, you get a list of all error values of the input result list. 1478 | 1479 | If only some results fail, the new combined error list will only contain the error value of the failed results, meaning that there is no guarantee of the length of the new error list. 1480 | 1481 | Function signature: 1482 | 1483 | ```typescript 1484 | // homogeneous lists 1485 | function combineWithAllErrors(resultList: ResultAsync[]): ResultAsync 1486 | 1487 | // heterogeneous lists 1488 | function combineWithAllErrors(resultList: [ ResultAsync, ResultAsync ]): ResultAsync<[ T1, T2 ], (E1 | E2)[]> 1489 | function combineWithAllErrors => ResultAsync<[ T1, T2, T3 ], (E1 | E2 | E3)[]> 1490 | function combineWithAllErrors => ResultAsync<[ T1, T2, T3, T4 ], (E1 | E2 | E3 | E4)[]> 1491 | // ... etc etc ad infinitum 1492 | ``` 1493 | 1494 | Example usage: 1495 | 1496 | ```typescript 1497 | const resultList: ResultAsync[] = [ 1498 | okAsync(123), 1499 | errAsync('boooom!'), 1500 | okAsync(456), 1501 | errAsync('ahhhhh!'), 1502 | ] 1503 | 1504 | const result = ResultAsync.combineWithAllErrors(resultList) 1505 | 1506 | // result is Err(['boooom!', 'ahhhhh!']) 1507 | ``` 1508 | 1509 | #### `ResultAsync.safeUnwrap()` 1510 | 1511 | **Deprecated**. You don't need to use this method anymore. 1512 | 1513 | Allows for unwrapping a `Result` or returning an `Err` implicitly, thereby reducing boilerplate. 1514 | 1515 | [⬆️ Back to top](#toc) 1516 | 1517 | --- 1518 | 1519 | ### Utilities 1520 | 1521 | #### `fromThrowable` 1522 | 1523 | Top level export of `Result.fromThrowable`. 1524 | Please find documentation at [Result.fromThrowable](#resultfromthrowable-static-class-method) 1525 | 1526 | [⬆️ Back to top](#toc) 1527 | 1528 | #### `fromAsyncThrowable` 1529 | 1530 | Top level export of `ResultAsync.fromThrowable`. 1531 | Please find documentation at [ResultAsync.fromThrowable](#resultasyncfromthrowable-static-class-method) 1532 | 1533 | [⬆️ Back to top](#toc) 1534 | 1535 | #### `fromPromise` 1536 | 1537 | Top level export of `ResultAsync.fromPromise`. 1538 | Please find documentation at [ResultAsync.fromPromise](#resultasyncfrompromise-static-class-method) 1539 | 1540 | [⬆️ Back to top](#toc) 1541 | 1542 | #### `fromSafePromise` 1543 | 1544 | Top level export of `ResultAsync.fromSafePromise`. 1545 | Please find documentation at [ResultAsync.fromSafePromise](#resultasyncfromsafepromise-static-class-method) 1546 | 1547 | [⬆️ Back to top](#toc) 1548 | 1549 | #### `safeTry` 1550 | 1551 | Used to implicitly return errors and reduce boilerplate. 1552 | 1553 | Let's say we are writing a function that returns a `Result`, and in that function we call some functions which also return `Result`s and we check those results to see whether we should keep going or abort. Usually, we will write like the following. 1554 | ```typescript 1555 | declare function mayFail1(): Result; 1556 | declare function mayFail2(): Result; 1557 | 1558 | function myFunc(): Result { 1559 | // We have to define a constant to hold the result to check and unwrap its value. 1560 | const result1 = mayFail1(); 1561 | if (result1.isErr()) { 1562 | return err(`aborted by an error from 1st function, ${result1.error}`); 1563 | } 1564 | const value1 = result1.value 1565 | 1566 | // Again, we need to define a constant and then check and unwrap. 1567 | const result2 = mayFail2(); 1568 | if (result2.isErr()) { 1569 | return err(`aborted by an error from 2nd function, ${result2.error}`); 1570 | } 1571 | const value2 = result2.value 1572 | 1573 | // And finally we return what we want to calculate 1574 | return ok(value1 + value2); 1575 | } 1576 | ``` 1577 | Basically, we need to define a constant for each result to check whether it's a `Ok` and read its `.value` or `.error`. 1578 | 1579 | With safeTry, we can state 'Return here if its an `Err`, otherwise unwrap it here and keep going.' in just one expression. 1580 | ```typescript 1581 | declare function mayFail1(): Result; 1582 | declare function mayFail2(): Result; 1583 | 1584 | function myFunc(): Result { 1585 | return safeTry(function*() { 1586 | return ok( 1587 | // If the result of mayFail1().mapErr() is an `Err`, the evaluation is 1588 | // aborted here and the enclosing `safeTry` block is evaluated to that `Err`. 1589 | // Otherwise, this `(yield* ...)` is evaluated to its `.value`. 1590 | (yield* mayFail1() 1591 | .mapErr(e => `aborted by an error from 1st function, ${e}`)) 1592 | + 1593 | // The same as above. 1594 | (yield* mayFail2() 1595 | .mapErr(e => `aborted by an error from 2nd function, ${e}`)) 1596 | ) 1597 | }) 1598 | } 1599 | ``` 1600 | 1601 | To use `safeTry`, the points are as follows. 1602 | * Wrap the entire block in a [generator function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function*) 1603 | * In that block, you can use `yield* ` to state 'Return `` if it's an `Err`, otherwise evaluate to its `.value`' 1604 | * Pass the generator function to `safeTry` 1605 | 1606 | You can also use [async generator function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function*) to pass an async block to `safeTry`. 1607 | ```typescript 1608 | // You can use either Promise or ResultAsync. 1609 | declare function mayFail1(): Promise>; 1610 | declare function mayFail2(): ResultAsync; 1611 | 1612 | function myFunc(): Promise> { 1613 | return safeTry(async function*() { 1614 | return ok( 1615 | // You have to await if the expression is Promise 1616 | (yield* (await mayFail1()) 1617 | .mapErr(e => `aborted by an error from 1st function, ${e}`)) 1618 | + 1619 | // You can call `safeUnwrap` directly if its ResultAsync 1620 | (yield* mayFail2() 1621 | .mapErr(e => `aborted by an error from 2nd function, ${e}`)) 1622 | ) 1623 | }) 1624 | } 1625 | ``` 1626 | 1627 | For more information, see https://github.com/supermacro/neverthrow/pull/448 and https://github.com/supermacro/neverthrow/issues/444 1628 | 1629 | [⬆️ Back to top](#toc) 1630 | 1631 | --- 1632 | 1633 | ### Testing 1634 | 1635 | `Result` instances have two unsafe methods, aptly called `_unsafeUnwrap` and `_unsafeUnwrapErr` which **should only be used in a test environment**. 1636 | 1637 | `_unsafeUnwrap` takes a `Result` and returns a `T` when the result is an `Ok`, otherwise it throws a custom object. 1638 | 1639 | `_unsafeUnwrapErr` takes a `Result` and returns a `E` when the result is an `Err`, otherwise it throws a custom object. 1640 | 1641 | That way you can do something like: 1642 | 1643 | ```typescript 1644 | expect(myResult._unsafeUnwrap()).toBe(someExpectation) 1645 | ``` 1646 | 1647 | However, do note that `Result` instances are comparable. So you don't necessarily need to unwrap them in order to assert expectations in your tests. So you could also do something like this: 1648 | 1649 | ```typescript 1650 | import { ok } from 'neverthrow' 1651 | 1652 | // ... 1653 | 1654 | expect(callSomeFunctionThatReturnsAResult("with", "some", "args")).toEqual(ok(someExpectation)); 1655 | ``` 1656 | 1657 | By default, the thrown value does not contain a stack trace. This is because stack trace generation [makes error messages in Jest harder to understand](https://github.com/supermacro/neverthrow/pull/215). If you want stack traces to be generated, call `_unsafeUnwrap` and / or `_unsafeUnwrapErr` with a config object: 1658 | 1659 | ```typescript 1660 | _unsafeUnwrapErr({ 1661 | withStackTrace: true, 1662 | }) 1663 | 1664 | // ^ Now the error object will have a `.stack` property containing the current stack 1665 | ``` 1666 | 1667 | --- 1668 | 1669 | If you find this package useful, please consider [sponsoring me](https://github.com/sponsors/supermacro/) or simply [buying me a coffee](https://ko-fi.com/gdelgado)! 1670 | 1671 | --- 1672 | 1673 | ## A note on the Package Name 1674 | 1675 | Although the package is called `neverthrow`, please don't take this literally. I am simply encouraging the developer to think a bit more about the ergonomics and usage of whatever software they are writing. 1676 | 1677 | `Throw`ing and `catching` is very similar to using `goto` statements - in other words; it makes reasoning about your programs harder. Secondly, by using `throw` you make the assumption that the caller of your function is implementing `catch`. This is a known source of errors. Example: One dev `throw`s and another dev uses the function without prior knowledge that the function will throw. Thus, and edge case has been left unhandled and now you have unhappy users, bosses, cats, etc. 1678 | 1679 | With all that said, there are definitely good use cases for throwing in your program. But much less than you might think. 1680 | 1681 | ### License 1682 | 1683 | The neverthrow project is available as open source under the terms of the [MIT license](https://github.com/supermacro/neverthrow/blob/master/LICENSE). 1684 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "neverthrow", 3 | "version": "8.2.0", 4 | "description": "Stop throwing errors, and instead return Results!", 5 | "main": "dist/index.cjs.js", 6 | "module": "dist/index.es.js", 7 | "types": "dist/index.d.ts", 8 | "files": [ 9 | "dist" 10 | ], 11 | "scripts": { 12 | "local-ci": "npm run typecheck && npm run lint && npm run test && npm run format && npm run build", 13 | "test": "vitest run && npm run test-types", 14 | "test-types": "tsc --noEmit -p ./tests/tsconfig.tests.json", 15 | "lint": "eslint ./src --ext .ts", 16 | "format": "prettier --write 'src/**/*.ts?(x)' && npm run lint -- --fix", 17 | "typecheck": "tsc --noEmit", 18 | "clean": "rm -rf ./dist ./tmp", 19 | "build": "npm run clean && rollup --config && mv tmp/*.js dist && attw --pack .", 20 | "prepublishOnly": "npm run build", 21 | "release": "changeset publish", 22 | "version": "changeset version && npm i --lockfile-only" 23 | }, 24 | "repository": { 25 | "type": "git", 26 | "url": "git+https://github.com/supermacro/neverthrow.git" 27 | }, 28 | "author": "Giorgio Delgado", 29 | "license": "MIT", 30 | "bugs": { 31 | "url": "https://github.com/supermacro/neverthrow/issues" 32 | }, 33 | "homepage": "https://github.com/supermacro/neverthrow#readme", 34 | "devDependencies": { 35 | "@arethetypeswrong/cli": "^0.17.3", 36 | "@changesets/changelog-github": "^0.5.0", 37 | "@changesets/cli": "^2.27.7", 38 | "@types/node": "^18.19.39", 39 | "@typescript-eslint/eslint-plugin": "4.28.1", 40 | "@typescript-eslint/parser": "4.28.1", 41 | "eslint": "7.30.0", 42 | "eslint-config-prettier": "7.1.0", 43 | "eslint-plugin-prettier": "3.4.0", 44 | "prettier": "2.2.1", 45 | "rollup": "^4.18.0", 46 | "rollup-plugin-dts": "^6.1.1", 47 | "rollup-plugin-typescript2": "^0.32.1", 48 | "testdouble": "3.20.2", 49 | "ts-toolbelt": "9.6.0", 50 | "typescript": "4.7.2", 51 | "vitest": "^2.1.3" 52 | }, 53 | "keywords": [ 54 | "typescript", 55 | "functional", 56 | "fp", 57 | "error" 58 | ], 59 | "engines": { 60 | "node": ">=18", 61 | "npm": ">=11" 62 | }, 63 | "optionalDependencies": { 64 | "@rollup/rollup-linux-x64-gnu": "^4.24.0" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /rollup.config.mjs: -------------------------------------------------------------------------------- 1 | import typescript from "rollup-plugin-typescript2"; 2 | import dts from "rollup-plugin-dts"; 3 | 4 | /** 5 | * Just a few details about this build implementation: 6 | * - Uses `rollup` along with `rollup-plugin-typescript2` to build to two target files 7 | * (`index.es.js` and `index.cjs.js`). This means that there is only a *single* file now for each 8 | * target rather than 4 files (ie. `index.js`, `chain.js`, etc.). 9 | * - Rollup has their own official @rollup/plugin-typescript plugin but it does not have the best 10 | * TS declarations support atm (see https://github.com/rollup/plugins/issues/394, 11 | * https://github.com/rollup/plugins/issues/254, https://github.com/rollup/plugins/issues/243). 12 | * Until these are resolved, I choose to just use `rollup-plugin-typescript2`. 13 | * - `rollup-plugin-typescript2` generates `index.d.ts`, `chain.d.ts`, `result.d.ts` and 14 | * `result-async.d.ts` but this is inconsistent with the generated JavaScript files. This isn't a 15 | * huge issues unless someone tries to import `neverthrow/dist/chain` which would error 16 | * because the underlying `.js` doesn't exist. To remedy this issue, I used `rollup-plugin-dts` to 17 | * merge `*.d.ts` files into a single `index.d.ts`. 18 | * - It's unfortunately a bit complicated to generate two build outputs but once some of the 19 | * issues I've linked to above are resolved this process will become much easier :) 20 | * - Because the build process is kinda two steps (first using `rollup-plugin-typescript2` and then 21 | * using `rollup-plugin-dts`), I first write to `tmp/`, then write the merged `index.d.ts` to 22 | * `dist/` and then moved `index.es.js` and `index.cjs.js` to `dist/`. 23 | */ 24 | 25 | export default [ 26 | { 27 | input: "src/index.ts", 28 | output: { 29 | file: "tmp/index.es.js", 30 | format: "es", 31 | }, 32 | plugins: [typescript()], 33 | }, 34 | { 35 | input: "src/index.ts", 36 | output: { 37 | file: "tmp/index.cjs.js", 38 | format: "cjs", 39 | }, 40 | plugins: [typescript()], 41 | }, 42 | { 43 | input: "tmp/index.d.ts", 44 | output: [{ file: "dist/index.d.ts", format: "es" }], 45 | plugins: [dts()], 46 | }, 47 | ]; 48 | -------------------------------------------------------------------------------- /src/_internals/error.ts: -------------------------------------------------------------------------------- 1 | import { Result } from '../result' 2 | 3 | export interface ErrorConfig { 4 | withStackTrace: boolean 5 | } 6 | 7 | const defaultErrorConfig: ErrorConfig = { 8 | withStackTrace: false, 9 | } 10 | 11 | interface NeverThrowError { 12 | data: 13 | | { 14 | type: string 15 | value: T 16 | } 17 | | { 18 | type: string 19 | value: E 20 | } 21 | message: string 22 | stack: string | undefined 23 | } 24 | 25 | // Custom error object 26 | // Context / discussion: https://github.com/supermacro/neverthrow/pull/215 27 | export const createNeverThrowError = ( 28 | message: string, 29 | result: Result, 30 | config: ErrorConfig = defaultErrorConfig, 31 | ): NeverThrowError => { 32 | const data = result.isOk() 33 | ? { type: 'Ok', value: result.value } 34 | : { type: 'Err', value: result.error } 35 | 36 | const maybeStack = config.withStackTrace ? new Error().stack : undefined 37 | 38 | return { 39 | data, 40 | message, 41 | stack: maybeStack, 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/_internals/utils.ts: -------------------------------------------------------------------------------- 1 | import { Result, ok, err } from '../result' 2 | import { ResultAsync } from '../result-async' 3 | 4 | // Given a list of Results, this extracts all the different `T` types from that list 5 | export type ExtractOkTypes[]> = { 6 | [idx in keyof T]: T[idx] extends Result ? U : never 7 | } 8 | 9 | // Given a list of ResultAsyncs, this extracts all the different `T` types from that list 10 | export type ExtractOkAsyncTypes[]> = { 11 | [idx in keyof T]: T[idx] extends ResultAsync ? U : never 12 | } 13 | 14 | // Given a list of Results, this extracts all the different `E` types from that list 15 | export type ExtractErrTypes[]> = { 16 | [idx in keyof T]: T[idx] extends Result ? E : never 17 | } 18 | 19 | // Given a list of ResultAsyncs, this extracts all the different `E` types from that list 20 | export type ExtractErrAsyncTypes[]> = { 21 | [idx in keyof T]: T[idx] extends ResultAsync ? E : never 22 | } 23 | 24 | export type InferOkTypes = R extends Result ? T : never 25 | export type InferErrTypes = R extends Result ? E : never 26 | 27 | export type InferAsyncOkTypes = R extends ResultAsync ? T : never 28 | export type InferAsyncErrTypes = R extends ResultAsync ? E : never 29 | 30 | /** 31 | * Short circuits on the FIRST Err value that we find 32 | */ 33 | export const combineResultList = ( 34 | resultList: readonly Result[], 35 | ): Result => { 36 | let acc = ok([]) as Result 37 | 38 | for (const result of resultList) { 39 | if (result.isErr()) { 40 | acc = err(result.error) 41 | break 42 | } else { 43 | acc.map((list) => list.push(result.value)) 44 | } 45 | } 46 | return acc 47 | } 48 | 49 | /* This is the typesafe version of Promise.all 50 | * 51 | * Takes a list of ResultAsync and success if all inner results are Ok values 52 | * or fails if one (or more) of the inner results are Err values 53 | */ 54 | export const combineResultAsyncList = ( 55 | asyncResultList: readonly ResultAsync[], 56 | ): ResultAsync => 57 | ResultAsync.fromSafePromise(Promise.all(asyncResultList)).andThen( 58 | combineResultList, 59 | ) as ResultAsync 60 | 61 | /** 62 | * Give a list of all the errors we find 63 | */ 64 | export const combineResultListWithAllErrors = ( 65 | resultList: readonly Result[], 66 | ): Result => { 67 | let acc = ok([]) as Result 68 | 69 | for (const result of resultList) { 70 | if (result.isErr() && acc.isErr()) { 71 | acc.error.push(result.error) 72 | } else if (result.isErr() && acc.isOk()) { 73 | acc = err([result.error]) 74 | } else if (result.isOk() && acc.isOk()) { 75 | acc.value.push(result.value) 76 | } 77 | // do nothing when result.isOk() && acc.isErr() 78 | } 79 | return acc 80 | } 81 | 82 | export const combineResultAsyncListWithAllErrors = ( 83 | asyncResultList: readonly ResultAsync[], 84 | ): ResultAsync => 85 | ResultAsync.fromSafePromise(Promise.all(asyncResultList)).andThen( 86 | combineResultListWithAllErrors, 87 | ) as ResultAsync 88 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { Result, ok, Ok, err, Err, fromThrowable, safeTry } from './result' 2 | export { 3 | ResultAsync, 4 | okAsync, 5 | errAsync, 6 | fromAsyncThrowable, 7 | fromPromise, 8 | fromSafePromise, 9 | } from './result-async' 10 | -------------------------------------------------------------------------------- /src/result-async.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | Combine, 3 | Dedup, 4 | EmptyArrayToNever, 5 | IsLiteralArray, 6 | MemberListOf, 7 | MembersToUnion, 8 | } from './result' 9 | 10 | import { Err, Ok, Result } from './' 11 | import { 12 | combineResultAsyncList, 13 | combineResultAsyncListWithAllErrors, 14 | ExtractErrAsyncTypes, 15 | ExtractOkAsyncTypes, 16 | InferAsyncErrTypes, 17 | InferAsyncOkTypes, 18 | InferErrTypes, 19 | InferOkTypes, 20 | } from './_internals/utils' 21 | 22 | export class ResultAsync implements PromiseLike> { 23 | private _promise: Promise> 24 | 25 | constructor(res: Promise>) { 26 | this._promise = res 27 | } 28 | 29 | static fromSafePromise(promise: PromiseLike): ResultAsync 30 | static fromSafePromise(promise: Promise): ResultAsync { 31 | const newPromise = promise.then((value: T) => new Ok(value)) 32 | 33 | return new ResultAsync(newPromise) 34 | } 35 | 36 | static fromPromise(promise: PromiseLike, errorFn: (e: unknown) => E): ResultAsync 37 | static fromPromise(promise: Promise, errorFn: (e: unknown) => E): ResultAsync { 38 | const newPromise = promise 39 | .then((value: T) => new Ok(value)) 40 | .catch((e) => new Err(errorFn(e))) 41 | 42 | return new ResultAsync(newPromise) 43 | } 44 | 45 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 46 | static fromThrowable( 47 | fn: (...args: A) => Promise, 48 | errorFn?: (err: unknown) => E, 49 | ): (...args: A) => ResultAsync { 50 | return (...args) => { 51 | return new ResultAsync( 52 | (async () => { 53 | try { 54 | return new Ok(await fn(...args)) 55 | } catch (error) { 56 | return new Err(errorFn ? errorFn(error) : error) 57 | } 58 | })(), 59 | ) 60 | } 61 | } 62 | 63 | static combine< 64 | T extends readonly [ResultAsync, ...ResultAsync[]] 65 | >(asyncResultList: T): CombineResultAsyncs 66 | static combine[]>( 67 | asyncResultList: T, 68 | ): CombineResultAsyncs 69 | static combine[]>( 70 | asyncResultList: T, 71 | ): CombineResultAsyncs { 72 | return (combineResultAsyncList(asyncResultList) as unknown) as CombineResultAsyncs 73 | } 74 | 75 | static combineWithAllErrors< 76 | T extends readonly [ResultAsync, ...ResultAsync[]] 77 | >(asyncResultList: T): CombineResultsWithAllErrorsArrayAsync 78 | static combineWithAllErrors[]>( 79 | asyncResultList: T, 80 | ): CombineResultsWithAllErrorsArrayAsync 81 | static combineWithAllErrors[]>( 82 | asyncResultList: T, 83 | ): CombineResultsWithAllErrorsArrayAsync { 84 | return combineResultAsyncListWithAllErrors( 85 | asyncResultList, 86 | ) as CombineResultsWithAllErrorsArrayAsync 87 | } 88 | 89 | map(f: (t: T) => A | Promise): ResultAsync { 90 | return new ResultAsync( 91 | this._promise.then(async (res: Result) => { 92 | if (res.isErr()) { 93 | return new Err(res.error) 94 | } 95 | 96 | return new Ok(await f(res.value)) 97 | }), 98 | ) 99 | } 100 | 101 | andThrough(f: (t: T) => Result | ResultAsync): ResultAsync { 102 | return new ResultAsync( 103 | this._promise.then(async (res: Result) => { 104 | if (res.isErr()) { 105 | return new Err(res.error) 106 | } 107 | 108 | const newRes = await f(res.value) 109 | if (newRes.isErr()) { 110 | return new Err(newRes.error) 111 | } 112 | return new Ok(res.value) 113 | }), 114 | ) 115 | } 116 | 117 | andTee(f: (t: T) => unknown): ResultAsync { 118 | return new ResultAsync( 119 | this._promise.then(async (res: Result) => { 120 | if (res.isErr()) { 121 | return new Err(res.error) 122 | } 123 | try { 124 | await f(res.value) 125 | } catch (e) { 126 | // Tee does not care about the error 127 | } 128 | return new Ok(res.value) 129 | }), 130 | ) 131 | } 132 | 133 | orTee(f: (t: E) => unknown): ResultAsync { 134 | return new ResultAsync( 135 | this._promise.then(async (res: Result) => { 136 | if (res.isOk()) { 137 | return new Ok(res.value) 138 | } 139 | try { 140 | await f(res.error) 141 | } catch (e) { 142 | // Tee does not care about the error 143 | } 144 | return new Err(res.error) 145 | }), 146 | ) 147 | } 148 | 149 | mapErr(f: (e: E) => U | Promise): ResultAsync { 150 | return new ResultAsync( 151 | this._promise.then(async (res: Result) => { 152 | if (res.isOk()) { 153 | return new Ok(res.value) 154 | } 155 | 156 | return new Err(await f(res.error)) 157 | }), 158 | ) 159 | } 160 | 161 | andThen>( 162 | f: (t: T) => R, 163 | ): ResultAsync, InferErrTypes | E> 164 | andThen>( 165 | f: (t: T) => R, 166 | ): ResultAsync, InferAsyncErrTypes | E> 167 | andThen(f: (t: T) => Result | ResultAsync): ResultAsync 168 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types 169 | andThen(f: any): any { 170 | return new ResultAsync( 171 | this._promise.then((res) => { 172 | if (res.isErr()) { 173 | return new Err(res.error) 174 | } 175 | 176 | const newValue = f(res.value) 177 | return newValue instanceof ResultAsync ? newValue._promise : newValue 178 | }), 179 | ) 180 | } 181 | 182 | orElse>( 183 | f: (e: E) => R, 184 | ): ResultAsync | T, InferErrTypes> 185 | orElse>( 186 | f: (e: E) => R, 187 | ): ResultAsync | T, InferAsyncErrTypes> 188 | orElse(f: (e: E) => Result | ResultAsync): ResultAsync 189 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types 190 | orElse(f: any): any { 191 | return new ResultAsync( 192 | this._promise.then(async (res: Result) => { 193 | if (res.isErr()) { 194 | return f(res.error) 195 | } 196 | 197 | return new Ok(res.value) 198 | }), 199 | ) 200 | } 201 | 202 | match(ok: (t: T) => A, _err: (e: E) => B): Promise { 203 | return this._promise.then((res) => res.match(ok, _err)) 204 | } 205 | 206 | unwrapOr(t: A): Promise { 207 | return this._promise.then((res) => res.unwrapOr(t)) 208 | } 209 | 210 | /** 211 | * @deprecated will be removed in 9.0.0. 212 | * 213 | * You can use `safeTry` without this method. 214 | * @example 215 | * ```typescript 216 | * safeTry(async function* () { 217 | * const okValue = yield* yourResult 218 | * }) 219 | * ``` 220 | * Emulates Rust's `?` operator in `safeTry`'s body. See also `safeTry`. 221 | */ 222 | async *safeUnwrap(): AsyncGenerator, T> { 223 | return yield* await this._promise.then((res) => res.safeUnwrap()) 224 | } 225 | 226 | // Makes ResultAsync implement PromiseLike 227 | then( 228 | successCallback?: (res: Result) => A | PromiseLike, 229 | failureCallback?: (reason: unknown) => B | PromiseLike, 230 | ): PromiseLike { 231 | return this._promise.then(successCallback, failureCallback) 232 | } 233 | 234 | async *[Symbol.asyncIterator](): AsyncGenerator, T> { 235 | const result = await this._promise 236 | 237 | if (result.isErr()) { 238 | // @ts-expect-error -- This is structurally equivalent and safe 239 | yield errAsync(result.error) 240 | } 241 | 242 | // @ts-expect-error -- This is structurally equivalent and safe 243 | return result.value 244 | } 245 | } 246 | 247 | export function okAsync(value: T): ResultAsync 248 | export function okAsync(value: void): ResultAsync 249 | export function okAsync(value: T): ResultAsync { 250 | return new ResultAsync(Promise.resolve(new Ok(value))) 251 | } 252 | 253 | export function errAsync(err: E): ResultAsync 254 | export function errAsync(err: void): ResultAsync 255 | export function errAsync(err: E): ResultAsync { 256 | return new ResultAsync(Promise.resolve(new Err(err))) 257 | } 258 | 259 | export const fromPromise = ResultAsync.fromPromise 260 | export const fromSafePromise = ResultAsync.fromSafePromise 261 | 262 | export const fromAsyncThrowable = ResultAsync.fromThrowable 263 | 264 | // Combines the array of async results into one result. 265 | export type CombineResultAsyncs< 266 | T extends readonly ResultAsync[] 267 | > = IsLiteralArray extends 1 268 | ? TraverseAsync> 269 | : ResultAsync, ExtractErrAsyncTypes[number]> 270 | 271 | // Combines the array of async results into one result with all errors. 272 | export type CombineResultsWithAllErrorsArrayAsync< 273 | T extends readonly ResultAsync[] 274 | > = IsLiteralArray extends 1 275 | ? TraverseWithAllErrorsAsync> 276 | : ResultAsync, ExtractErrAsyncTypes[number][]> 277 | 278 | // Unwraps the inner `Result` from a `ResultAsync` for all elements. 279 | type UnwrapAsync = IsLiteralArray extends 1 280 | ? Writable extends [infer H, ...infer Rest] 281 | ? H extends PromiseLike 282 | ? HI extends Result 283 | ? [Dedup, ...UnwrapAsync] 284 | : never 285 | : never 286 | : [] 287 | : // If we got something too general such as ResultAsync[] then we 288 | // simply need to map it to ResultAsync. Yet `ResultAsync` 289 | // itself is a union therefore it would be enough to cast it to Ok. 290 | T extends Array 291 | ? A extends PromiseLike 292 | ? HI extends Result 293 | ? Ok[] 294 | : never 295 | : never 296 | : never 297 | 298 | // Traverse through the tuples of the async results and create one 299 | // `ResultAsync` where the collected tuples are merged. 300 | type TraverseAsync = IsLiteralArray extends 1 301 | ? Combine extends [infer Oks, infer Errs] 302 | ? ResultAsync, MembersToUnion> 303 | : never 304 | : // The following check is important if we somehow reach to the point of 305 | // checking something similar to ResultAsync[]. In this case we don't 306 | // know the length of the elements, therefore we need to traverse the X and Y 307 | // in a way that the result should contain X[] and Y[]. 308 | T extends Array 309 | ? // The MemberListOf here is to include all possible types. Therefore 310 | // if we face (ResultAsync | ResultAsync)[] this type should 311 | // handle the case. 312 | Combine, Depth> extends [infer Oks, infer Errs] 313 | ? // The following `extends unknown[]` checks are just to satisfy the TS. 314 | // we already expect them to be an array. 315 | Oks extends unknown[] 316 | ? Errs extends unknown[] 317 | ? ResultAsync, MembersToUnion> 318 | : ResultAsync, Errs> 319 | : // The rest of the conditions are to satisfy the TS and support 320 | // the edge cases which are not really expected to happen. 321 | Errs extends unknown[] 322 | ? ResultAsync> 323 | : ResultAsync 324 | : never 325 | : never 326 | 327 | // This type is similar to the `TraverseAsync` while the errors are also 328 | // collected in a list. For the checks/conditions made here, see that type 329 | // for the documentation. 330 | type TraverseWithAllErrorsAsync = TraverseAsync< 331 | T, 332 | Depth 333 | > extends ResultAsync 334 | ? ResultAsync 335 | : never 336 | 337 | // Converts a reaodnly array into a writable array 338 | type Writable = T extends ReadonlyArray ? [...T] : T 339 | -------------------------------------------------------------------------------- /src/result.ts: -------------------------------------------------------------------------------- 1 | import { errAsync, ResultAsync } from './' 2 | import { createNeverThrowError, ErrorConfig } from './_internals/error' 3 | import { 4 | combineResultList, 5 | combineResultListWithAllErrors, 6 | ExtractErrTypes, 7 | ExtractOkTypes, 8 | InferAsyncErrTypes, 9 | InferErrTypes, 10 | InferOkTypes, 11 | } from './_internals/utils' 12 | 13 | // eslint-disable-next-line @typescript-eslint/no-namespace 14 | export namespace Result { 15 | /** 16 | * Wraps a function with a try catch, creating a new function with the same 17 | * arguments but returning `Ok` if successful, `Err` if the function throws 18 | * 19 | * @param fn function to wrap with ok on success or err on failure 20 | * @param errorFn when an error is thrown, this will wrap the error result if provided 21 | */ 22 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 23 | export function fromThrowable any, E>( 24 | fn: Fn, 25 | errorFn?: (e: unknown) => E, 26 | ): (...args: Parameters) => Result, E> { 27 | return (...args) => { 28 | try { 29 | const result = fn(...args) 30 | return ok(result) 31 | } catch (e) { 32 | return err(errorFn ? errorFn(e) : e) 33 | } 34 | } 35 | } 36 | 37 | export function combine< 38 | T extends readonly [Result, ...Result[]] 39 | >(resultList: T): CombineResults 40 | export function combine[]>( 41 | resultList: T, 42 | ): CombineResults 43 | export function combine< 44 | T extends readonly [Result, ...Result[]] 45 | >(resultList: T): CombineResults { 46 | return combineResultList(resultList) as CombineResults 47 | } 48 | 49 | export function combineWithAllErrors< 50 | T extends readonly [Result, ...Result[]] 51 | >(resultList: T): CombineResultsWithAllErrorsArray 52 | export function combineWithAllErrors[]>( 53 | resultList: T, 54 | ): CombineResultsWithAllErrorsArray 55 | export function combineWithAllErrors[]>( 56 | resultList: T, 57 | ): CombineResultsWithAllErrorsArray { 58 | return combineResultListWithAllErrors(resultList) as CombineResultsWithAllErrorsArray 59 | } 60 | } 61 | 62 | export type Result = Ok | Err 63 | 64 | export function ok(value: T): Ok 65 | export function ok(value: void): Ok 66 | export function ok(value: T): Ok { 67 | return new Ok(value) 68 | } 69 | 70 | export function err(err: E): Err 71 | export function err(err: E): Err 72 | export function err(err: void): Err 73 | export function err(err: E): Err { 74 | return new Err(err) 75 | } 76 | 77 | /** 78 | * Evaluates the given generator to a Result returned or an Err yielded from it, 79 | * whichever comes first. 80 | * 81 | * This function is intended to emulate Rust's ? operator. 82 | * See `/tests/safeTry.test.ts` for examples. 83 | * 84 | * @param body - What is evaluated. In body, `yield* result` works as 85 | * Rust's `result?` expression. 86 | * @returns The first occurrence of either an yielded Err or a returned Result. 87 | */ 88 | export function safeTry(body: () => Generator, Result>): Result 89 | export function safeTry< 90 | YieldErr extends Err, 91 | GeneratorReturnResult extends Result 92 | >( 93 | body: () => Generator, 94 | ): Result< 95 | InferOkTypes, 96 | InferErrTypes | InferErrTypes 97 | > 98 | 99 | /** 100 | * Evaluates the given generator to a Result returned or an Err yielded from it, 101 | * whichever comes first. 102 | * 103 | * This function is intended to emulate Rust's ? operator. 104 | * See `/tests/safeTry.test.ts` for examples. 105 | * 106 | * @param body - What is evaluated. In body, `yield* result` and 107 | * `yield* resultAsync` work as Rust's `result?` expression. 108 | * @returns The first occurrence of either an yielded Err or a returned Result. 109 | */ 110 | export function safeTry( 111 | body: () => AsyncGenerator, Result>, 112 | ): ResultAsync 113 | export function safeTry< 114 | YieldErr extends Err, 115 | GeneratorReturnResult extends Result 116 | >( 117 | body: () => AsyncGenerator, 118 | ): ResultAsync< 119 | InferOkTypes, 120 | InferErrTypes | InferErrTypes 121 | > 122 | export function safeTry( 123 | body: 124 | | (() => Generator, Result>) 125 | | (() => AsyncGenerator, Result>), 126 | ): Result | ResultAsync { 127 | const n = body().next() 128 | if (n instanceof Promise) { 129 | return new ResultAsync(n.then((r) => r.value)) 130 | } 131 | return n.value 132 | } 133 | 134 | interface IResult { 135 | /** 136 | * Used to check if a `Result` is an `OK` 137 | * 138 | * @returns `true` if the result is an `OK` variant of Result 139 | */ 140 | isOk(): this is Ok 141 | 142 | /** 143 | * Used to check if a `Result` is an `Err` 144 | * 145 | * @returns `true` if the result is an `Err` variant of Result 146 | */ 147 | isErr(): this is Err 148 | 149 | /** 150 | * Maps a `Result` to `Result` 151 | * by applying a function to a contained `Ok` value, leaving an `Err` value 152 | * untouched. 153 | * 154 | * @param f The function to apply an `OK` value 155 | * @returns the result of applying `f` or an `Err` untouched 156 | */ 157 | map(f: (t: T) => A): Result 158 | 159 | /** 160 | * Maps a `Result` to `Result` by applying a function to a 161 | * contained `Err` value, leaving an `Ok` value untouched. 162 | * 163 | * This function can be used to pass through a successful result while 164 | * handling an error. 165 | * 166 | * @param f a function to apply to the error `Err` value 167 | */ 168 | mapErr(f: (e: E) => U): Result 169 | 170 | /** 171 | * Similar to `map` Except you must return a new `Result`. 172 | * 173 | * This is useful for when you need to do a subsequent computation using the 174 | * inner `T` value, but that computation might fail. 175 | * Additionally, `andThen` is really useful as a tool to flatten a 176 | * `Result, E1>` into a `Result` (see example below). 177 | * 178 | * @param f The function to apply to the current value 179 | */ 180 | andThen>( 181 | f: (t: T) => R, 182 | ): Result, InferErrTypes | E> 183 | andThen(f: (t: T) => Result): Result 184 | 185 | /** 186 | * This "tee"s the current value to an passed-in computation such as side 187 | * effect functions but still returns the same current value as the result. 188 | * 189 | * This is useful when you want to pass the current result to your side-track 190 | * work such as logging but want to continue main-track work after that. 191 | * This method does not care about the result of the passed in computation. 192 | * 193 | * @param f The function to apply to the current value 194 | */ 195 | andTee(f: (t: T) => unknown): Result 196 | 197 | /** 198 | * This "tee"s the current `Err` value to an passed-in computation such as side 199 | * effect functions but still returns the same `Err` value as the result. 200 | * 201 | * This is useful when you want to pass the current `Err` value to your side-track 202 | * work such as logging but want to continue error-track work after that. 203 | * This method does not care about the result of the passed in computation. 204 | * 205 | * @param f The function to apply to the current `Err` value 206 | */ 207 | orTee(f: (t: E) => unknown): Result 208 | 209 | /** 210 | * Similar to `andTee` except error result of the computation will be passed 211 | * to the downstream in case of an error. 212 | * 213 | * This version is useful when you want to make side-effects but in case of an 214 | * error, you want to pass the error to the downstream. 215 | * 216 | * @param f The function to apply to the current value 217 | */ 218 | andThrough>(f: (t: T) => R): Result | E> 219 | andThrough(f: (t: T) => Result): Result 220 | 221 | /** 222 | * Takes an `Err` value and maps it to a `Result`. 223 | * 224 | * This is useful for error recovery. 225 | * 226 | * 227 | * @param f A function to apply to an `Err` value, leaving `Ok` values 228 | * untouched. 229 | */ 230 | orElse>( 231 | f: (e: E) => R, 232 | ): Result | T, InferErrTypes> 233 | orElse(f: (e: E) => Result): Result 234 | 235 | /** 236 | * Similar to `map` Except you must return a new `Result`. 237 | * 238 | * This is useful for when you need to do a subsequent async computation using 239 | * the inner `T` value, but that computation might fail. Must return a ResultAsync 240 | * 241 | * @param f The function that returns a `ResultAsync` to apply to the current 242 | * value 243 | */ 244 | asyncAndThen(f: (t: T) => ResultAsync): ResultAsync 245 | 246 | /** 247 | * Maps a `Result` to `ResultAsync` 248 | * by applying an async function to a contained `Ok` value, leaving an `Err` 249 | * value untouched. 250 | * 251 | * @param f An async function to apply an `OK` value 252 | */ 253 | asyncMap(f: (t: T) => Promise): ResultAsync 254 | 255 | /** 256 | * Unwrap the `Ok` value, or return the default if there is an `Err` 257 | * 258 | * @param v the default value to return if there is an `Err` 259 | */ 260 | unwrapOr(v: A): T | A 261 | 262 | /** 263 | * 264 | * Given 2 functions (one for the `Ok` variant and one for the `Err` variant) 265 | * execute the function that matches the `Result` variant. 266 | * 267 | * Match callbacks do not necessitate to return a `Result`, however you can 268 | * return a `Result` if you want to. 269 | * 270 | * `match` is like chaining `map` and `mapErr`, with the distinction that 271 | * with `match` both functions must have the same return type. 272 | * 273 | * @param ok 274 | * @param err 275 | */ 276 | match(ok: (t: T) => A, err: (e: E) => B): A | B 277 | 278 | /** 279 | * @deprecated will be removed in 9.0.0. 280 | * 281 | * You can use `safeTry` without this method. 282 | * @example 283 | * ```typescript 284 | * safeTry(function* () { 285 | * const okValue = yield* yourResult 286 | * }) 287 | * ``` 288 | * Emulates Rust's `?` operator in `safeTry`'s body. See also `safeTry`. 289 | */ 290 | safeUnwrap(): Generator, T> 291 | 292 | /** 293 | * **This method is unsafe, and should only be used in a test environments** 294 | * 295 | * Takes a `Result` and returns a `T` when the result is an `Ok`, otherwise it throws a custom object. 296 | * 297 | * @param config 298 | */ 299 | _unsafeUnwrap(config?: ErrorConfig): T 300 | 301 | /** 302 | * **This method is unsafe, and should only be used in a test environments** 303 | * 304 | * takes a `Result` and returns a `E` when the result is an `Err`, 305 | * otherwise it throws a custom object. 306 | * 307 | * @param config 308 | */ 309 | _unsafeUnwrapErr(config?: ErrorConfig): E 310 | } 311 | 312 | export class Ok implements IResult { 313 | constructor(readonly value: T) {} 314 | 315 | isOk(): this is Ok { 316 | return true 317 | } 318 | 319 | isErr(): this is Err { 320 | return !this.isOk() 321 | } 322 | 323 | map(f: (t: T) => A): Result { 324 | return ok(f(this.value)) 325 | } 326 | 327 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 328 | mapErr(_f: (e: E) => U): Result { 329 | return ok(this.value) 330 | } 331 | 332 | andThen>( 333 | f: (t: T) => R, 334 | ): Result, InferErrTypes | E> 335 | andThen(f: (t: T) => Result): Result 336 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types 337 | andThen(f: any): any { 338 | return f(this.value) 339 | } 340 | 341 | andThrough>(f: (t: T) => R): Result | E> 342 | andThrough(f: (t: T) => Result): Result 343 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types 344 | andThrough(f: any): any { 345 | return f(this.value).map((_value: unknown) => this.value) 346 | } 347 | 348 | andTee(f: (t: T) => unknown): Result { 349 | try { 350 | f(this.value) 351 | } catch (e) { 352 | // Tee doesn't care about the error 353 | } 354 | return ok(this.value) 355 | } 356 | 357 | orTee(_f: (t: E) => unknown): Result { 358 | return ok(this.value) 359 | } 360 | 361 | orElse>( 362 | _f: (e: E) => R, 363 | ): Result | T, InferErrTypes> 364 | orElse(_f: (e: E) => Result): Result 365 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types 366 | orElse(_f: any): any { 367 | return ok(this.value) 368 | } 369 | 370 | asyncAndThen(f: (t: T) => ResultAsync): ResultAsync { 371 | return f(this.value) 372 | } 373 | 374 | asyncAndThrough>( 375 | f: (t: T) => R, 376 | ): ResultAsync | E> 377 | asyncAndThrough(f: (t: T) => ResultAsync): ResultAsync 378 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types 379 | asyncAndThrough(f: (t: T) => ResultAsync): any { 380 | return f(this.value).map(() => this.value) 381 | } 382 | 383 | asyncMap(f: (t: T) => Promise): ResultAsync { 384 | return ResultAsync.fromSafePromise(f(this.value)) 385 | } 386 | 387 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 388 | unwrapOr(_v: A): T | A { 389 | return this.value 390 | } 391 | 392 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 393 | match(ok: (t: T) => A, _err: (e: E) => B): A | B { 394 | return ok(this.value) 395 | } 396 | 397 | safeUnwrap(): Generator, T> { 398 | const value = this.value 399 | /* eslint-disable-next-line require-yield */ 400 | return (function* () { 401 | return value 402 | })() 403 | } 404 | 405 | _unsafeUnwrap(_?: ErrorConfig): T { 406 | return this.value 407 | } 408 | 409 | _unsafeUnwrapErr(config?: ErrorConfig): E { 410 | throw createNeverThrowError('Called `_unsafeUnwrapErr` on an Ok', this, config) 411 | } 412 | 413 | // eslint-disable-next-line @typescript-eslint/no-this-alias, require-yield 414 | *[Symbol.iterator](): Generator, T> { 415 | return this.value 416 | } 417 | } 418 | 419 | export class Err implements IResult { 420 | constructor(readonly error: E) {} 421 | 422 | isOk(): this is Ok { 423 | return false 424 | } 425 | 426 | isErr(): this is Err { 427 | return !this.isOk() 428 | } 429 | 430 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 431 | map(_f: (t: T) => A): Result { 432 | return err(this.error) 433 | } 434 | 435 | mapErr(f: (e: E) => U): Result { 436 | return err(f(this.error)) 437 | } 438 | 439 | andThrough(_f: (t: T) => Result): Result { 440 | return err(this.error) 441 | } 442 | 443 | andTee(_f: (t: T) => unknown): Result { 444 | return err(this.error) 445 | } 446 | 447 | orTee(f: (t: E) => unknown): Result { 448 | try { 449 | f(this.error) 450 | } catch (e) { 451 | // Tee doesn't care about the error 452 | } 453 | return err(this.error) 454 | } 455 | 456 | andThen>( 457 | _f: (t: T) => R, 458 | ): Result, InferErrTypes | E> 459 | andThen(_f: (t: T) => Result): Result 460 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types 461 | andThen(_f: any): any { 462 | return err(this.error) 463 | } 464 | 465 | orElse>( 466 | f: (e: E) => R, 467 | ): Result | T, InferErrTypes> 468 | orElse(f: (e: E) => Result): Result 469 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types 470 | orElse(f: any): any { 471 | return f(this.error) 472 | } 473 | 474 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 475 | asyncAndThen(_f: (t: T) => ResultAsync): ResultAsync { 476 | return errAsync(this.error) 477 | } 478 | 479 | asyncAndThrough(_f: (t: T) => ResultAsync): ResultAsync { 480 | return errAsync(this.error) 481 | } 482 | 483 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 484 | asyncMap(_f: (t: T) => Promise): ResultAsync { 485 | return errAsync(this.error) 486 | } 487 | 488 | unwrapOr(v: A): T | A { 489 | return v 490 | } 491 | 492 | match(_ok: (t: T) => A, err: (e: E) => B): A | B { 493 | return err(this.error) 494 | } 495 | 496 | safeUnwrap(): Generator, T> { 497 | const error = this.error 498 | return (function* () { 499 | yield err(error) 500 | 501 | throw new Error('Do not use this generator out of `safeTry`') 502 | })() 503 | } 504 | 505 | _unsafeUnwrap(config?: ErrorConfig): T { 506 | throw createNeverThrowError('Called `_unsafeUnwrap` on an Err', this, config) 507 | } 508 | 509 | _unsafeUnwrapErr(_?: ErrorConfig): E { 510 | return this.error 511 | } 512 | 513 | *[Symbol.iterator](): Generator, T> { 514 | // eslint-disable-next-line @typescript-eslint/no-this-alias 515 | const self = this 516 | // @ts-expect-error -- This is structurally equivalent and safe 517 | yield self 518 | // @ts-expect-error -- This is structurally equivalent and safe 519 | return self 520 | } 521 | } 522 | 523 | export const fromThrowable = Result.fromThrowable 524 | 525 | //#region Combine - Types 526 | 527 | // This is a helper type to prevent infinite recursion in typing rules. 528 | // 529 | // Use this with your `depth` variable in your types. 530 | type Prev = [ 531 | never, 532 | 0, 533 | 1, 534 | 2, 535 | 3, 536 | 4, 537 | 5, 538 | 6, 539 | 7, 540 | 8, 541 | 9, 542 | 10, 543 | 11, 544 | 12, 545 | 13, 546 | 14, 547 | 15, 548 | 16, 549 | 17, 550 | 18, 551 | 19, 552 | 20, 553 | 21, 554 | 22, 555 | 23, 556 | 24, 557 | 25, 558 | 26, 559 | 27, 560 | 28, 561 | 29, 562 | 30, 563 | 31, 564 | 32, 565 | 33, 566 | 34, 567 | 35, 568 | 36, 569 | 37, 570 | 38, 571 | 39, 572 | 40, 573 | 41, 574 | 42, 575 | 43, 576 | 44, 577 | 45, 578 | 46, 579 | 47, 580 | 48, 581 | 49, 582 | ...0[] 583 | ] 584 | 585 | // Collects the results array into separate tuple array. 586 | // 587 | // T - The array of the results 588 | // Collected - The collected tuples. 589 | // Depth - The maximum depth. 590 | type CollectResults = [ 591 | Depth, 592 | ] extends [never] 593 | ? [] 594 | : T extends [infer H, ...infer Rest] 595 | ? // And test whether the head of the list is a result 596 | H extends Result 597 | ? // Continue collecting... 598 | CollectResults< 599 | // the rest of the elements 600 | Rest, 601 | // The collected 602 | [...Collected, [L, R]], 603 | // and one less of the current depth 604 | Prev[Depth] 605 | > 606 | : never // Impossible 607 | : Collected 608 | 609 | // Transposes an array 610 | // 611 | // A - The array source 612 | // Transposed - The collected transposed array 613 | // Depth - The maximum depth. 614 | export type Transpose< 615 | A, 616 | Transposed extends unknown[][] = [], 617 | Depth extends number = 10 618 | > = A extends [infer T, ...infer Rest] 619 | ? T extends [infer L, infer R] 620 | ? Transposed extends [infer PL, infer PR] 621 | ? PL extends unknown[] 622 | ? PR extends unknown[] 623 | ? Transpose 624 | : never 625 | : never 626 | : Transpose 627 | : Transposed 628 | : Transposed 629 | 630 | // Combines the both sides of the array of the results into a tuple of the 631 | // union of the ok types and the union of the err types. 632 | // 633 | // T - The array of the results 634 | // Depth - The maximum depth. 635 | export type Combine = Transpose, [], Depth> extends [ 636 | infer L, 637 | infer R, 638 | ] 639 | ? [UnknownMembersToNever, UnknownMembersToNever] 640 | : Transpose, [], Depth> extends [] 641 | ? [[], []] 642 | : never 643 | 644 | // Deduplicates the result, as the result type is a union of Err and Ok types. 645 | export type Dedup = T extends Result 646 | ? [unknown] extends [RL] 647 | ? Err 648 | : Ok 649 | : T 650 | 651 | // Given a union, this gives the array of the union members. 652 | export type MemberListOf = ( 653 | (T extends unknown ? (t: T) => T : never) extends infer U 654 | ? (U extends unknown ? (u: U) => unknown : never) extends (v: infer V) => unknown 655 | ? V 656 | : never 657 | : never 658 | ) extends (_: unknown) => infer W 659 | ? [...MemberListOf>, W] 660 | : [] 661 | 662 | // Converts an empty array to never. 663 | // 664 | // The second type parameter here will affect how to behave to `never[]`s. 665 | // If a precise type is required, pass `1` here so that it will resolve 666 | // a literal array such as `[ never, never ]`. Otherwise, set `0` or the default 667 | // type value will cause this to resolve the arrays containing only `never` 668 | // items as `never` only. 669 | export type EmptyArrayToNever = T extends [] 670 | ? never 671 | : NeverArrayToNever extends 1 672 | ? T extends [never, ...infer Rest] 673 | ? [EmptyArrayToNever] extends [never] 674 | ? never 675 | : T 676 | : T 677 | : T 678 | 679 | // Converts the `unknown` items of an array to `never`s. 680 | type UnknownMembersToNever = T extends [infer H, ...infer R] 681 | ? [[unknown] extends [H] ? never : H, ...UnknownMembersToNever] 682 | : T 683 | 684 | // Gets the member type of the array or never. 685 | export type MembersToUnion = T extends unknown[] ? T[number] : never 686 | 687 | // Checks if the given type is a literal array. 688 | export type IsLiteralArray = T extends { length: infer L } 689 | ? L extends number 690 | ? number extends L 691 | ? 0 692 | : 1 693 | : 0 694 | : 0 695 | 696 | // Traverses an array of results and returns a single result containing 697 | // the oks and errs union-ed/combined. 698 | type Traverse = Combine extends [infer Oks, infer Errs] 699 | ? Result, MembersToUnion> 700 | : never 701 | 702 | // Traverses an array of results and returns a single result containing 703 | // the oks combined and the array of errors combined. 704 | type TraverseWithAllErrors = Traverse extends Result< 705 | infer Oks, 706 | infer Errs 707 | > 708 | ? Result 709 | : never 710 | 711 | // Combines the array of results into one result. 712 | export type CombineResults< 713 | T extends readonly Result[] 714 | > = IsLiteralArray extends 1 715 | ? Traverse 716 | : Result, ExtractErrTypes[number]> 717 | 718 | // Combines the array of results into one result with all errors. 719 | export type CombineResultsWithAllErrorsArray< 720 | T extends readonly Result[] 721 | > = IsLiteralArray extends 1 722 | ? TraverseWithAllErrors 723 | : Result, ExtractErrTypes[number][]> 724 | 725 | //#endregion 726 | -------------------------------------------------------------------------------- /tests/index.test.ts: -------------------------------------------------------------------------------- 1 | import * as td from 'testdouble' 2 | 3 | import { 4 | err, 5 | Err, 6 | errAsync, 7 | fromAsyncThrowable, 8 | fromPromise, 9 | fromSafePromise, 10 | fromThrowable, 11 | ok, 12 | Ok, 13 | okAsync, 14 | Result, 15 | ResultAsync, 16 | } from '../src' 17 | 18 | import { vitest, describe, expect, it } from 'vitest' 19 | 20 | describe('Result.Ok', () => { 21 | it('Creates an Ok value', () => { 22 | const okVal = ok(12) 23 | 24 | expect(okVal.isOk()).toBe(true) 25 | expect(okVal.isErr()).toBe(false) 26 | expect(okVal).toBeInstanceOf(Ok) 27 | }) 28 | 29 | it('Creates an Ok value with null', () => { 30 | const okVal = ok(null) 31 | 32 | expect(okVal.isOk()).toBe(true) 33 | expect(okVal.isErr()).toBe(false) 34 | expect(okVal._unsafeUnwrap()).toBe(null) 35 | }) 36 | 37 | it('Creates an Ok value with undefined', () => { 38 | const okVal = ok(undefined) 39 | 40 | expect(okVal.isOk()).toBe(true) 41 | expect(okVal.isErr()).toBe(false) 42 | expect(okVal._unsafeUnwrap()).toBeUndefined() 43 | }) 44 | 45 | it('Is comparable', () => { 46 | expect(ok(42)).toEqual(ok(42)) 47 | expect(ok(42)).not.toEqual(ok(43)) 48 | }) 49 | 50 | it('Maps over an Ok value', () => { 51 | const okVal = ok(12) 52 | const mapFn = vitest.fn((number) => number.toString()) 53 | 54 | const mapped = okVal.map(mapFn) 55 | 56 | expect(mapped.isOk()).toBe(true) 57 | expect(mapped._unsafeUnwrap()).toBe('12') 58 | expect(mapFn).toHaveBeenCalledTimes(1) 59 | }) 60 | 61 | it('Skips `mapErr`', () => { 62 | const mapErrorFunc = vitest.fn((_error) => 'mapped error value') 63 | 64 | const notMapped = ok(12).mapErr(mapErrorFunc) 65 | 66 | expect(notMapped.isOk()).toBe(true) 67 | expect(mapErrorFunc).not.toHaveBeenCalledTimes(1) 68 | }) 69 | 70 | describe('andThen', () => { 71 | it('Maps to an Ok', () => { 72 | const okVal = ok(12) 73 | 74 | const flattened = okVal.andThen((_number) => { 75 | // ... 76 | // complex logic 77 | // ... 78 | return ok({ data: 'why not' }) 79 | }) 80 | 81 | expect(flattened.isOk()).toBe(true) 82 | expect(flattened._unsafeUnwrap()).toStrictEqual({ data: 'why not' }) 83 | }) 84 | 85 | it('Maps to an Err', () => { 86 | const okval = ok(12) 87 | 88 | const flattened = okval.andThen((_number) => { 89 | // ... 90 | // complex logic 91 | // ... 92 | return err('Whoopsies!') 93 | }) 94 | 95 | expect(flattened.isOk()).toBe(false) 96 | 97 | const nextFn = vitest.fn((_val) => ok('noop')) 98 | 99 | flattened.andThen(nextFn) 100 | 101 | expect(nextFn).not.toHaveBeenCalled() 102 | }) 103 | }) 104 | 105 | describe('andThrough', () => { 106 | it('Calls the passed function but returns an original ok', () => { 107 | const okVal = ok(12) 108 | const passedFn = vitest.fn((_number) => ok(undefined)) 109 | 110 | const thrued = okVal.andThrough(passedFn) 111 | expect(thrued.isOk()).toBe(true) 112 | expect(passedFn).toHaveBeenCalledTimes(1) 113 | expect(thrued._unsafeUnwrap()).toStrictEqual(12) 114 | }) 115 | 116 | it('Maps to an Err', () => { 117 | const okval = ok(12) 118 | 119 | const thrued = okval.andThen((_number) => { 120 | // ... 121 | // complex logic 122 | // ... 123 | return err('Whoopsies!') 124 | }) 125 | 126 | expect(thrued.isOk()).toBe(false) 127 | expect(thrued._unsafeUnwrapErr()).toStrictEqual('Whoopsies!') 128 | 129 | const nextFn = vitest.fn((_val) => ok('noop')) 130 | 131 | thrued.andThen(nextFn) 132 | 133 | expect(nextFn).not.toHaveBeenCalled() 134 | }) 135 | }) 136 | 137 | describe('andTee', () => { 138 | it('Calls the passed function but returns an original ok', () => { 139 | const okVal = ok(12) 140 | const passedFn = vitest.fn((_number) => {}) 141 | 142 | const teed = okVal.andTee(passedFn) 143 | 144 | expect(teed.isOk()).toBe(true) 145 | expect(passedFn).toHaveBeenCalledTimes(1) 146 | expect(teed._unsafeUnwrap()).toStrictEqual(12) 147 | }) 148 | it('returns an original ok even when the passed function fails', () => { 149 | const okVal = ok(12) 150 | const passedFn = vitest.fn((_number) => { 151 | throw new Error('OMG!') 152 | }) 153 | 154 | const teed = okVal.andTee(passedFn) 155 | 156 | expect(teed.isOk()).toBe(true) 157 | expect(passedFn).toHaveBeenCalledTimes(1) 158 | expect(teed._unsafeUnwrap()).toStrictEqual(12) 159 | }) 160 | }) 161 | 162 | describe('orTee', () => { 163 | it('Calls the passed function but returns an original err', () => { 164 | const errVal = err(12) 165 | const passedFn = vitest.fn((_number) => {}) 166 | 167 | const teed = errVal.orTee(passedFn) 168 | 169 | expect(teed.isErr()).toBe(true) 170 | expect(passedFn).toHaveBeenCalledTimes(1) 171 | expect(teed._unsafeUnwrapErr()).toStrictEqual(12) 172 | }) 173 | it('returns an original err even when the passed function fails', () => { 174 | const errVal = err(12) 175 | const passedFn = vitest.fn((_number) => { 176 | throw new Error('OMG!') 177 | }) 178 | 179 | const teed = errVal.orTee(passedFn) 180 | 181 | expect(teed.isErr()).toBe(true) 182 | expect(passedFn).toHaveBeenCalledTimes(1) 183 | expect(teed._unsafeUnwrapErr()).toStrictEqual(12) 184 | }) 185 | }) 186 | 187 | describe('asyncAndThrough', () => { 188 | it('Calls the passed function but returns an original ok as Async', async () => { 189 | const okVal = ok(12) 190 | const passedFn = vitest.fn((_number) => okAsync(undefined)) 191 | 192 | const teedAsync = okVal.asyncAndThrough(passedFn) 193 | expect(teedAsync).toBeInstanceOf(ResultAsync) 194 | const teed = await teedAsync 195 | expect(teed.isOk()).toBe(true) 196 | expect(passedFn).toHaveBeenCalledTimes(1) 197 | expect(teed._unsafeUnwrap()).toStrictEqual(12) 198 | }) 199 | 200 | it('Maps to an Err', async () => { 201 | const okval = ok(12) 202 | 203 | const teedAsync = okval.asyncAndThen((_number) => { 204 | // ... 205 | // complex logic 206 | // ... 207 | return errAsync('Whoopsies!') 208 | }) 209 | expect(teedAsync).toBeInstanceOf(ResultAsync) 210 | const teed = await teedAsync 211 | expect(teed.isOk()).toBe(false) 212 | expect(teed._unsafeUnwrapErr()).toStrictEqual('Whoopsies!') 213 | 214 | const nextFn = vitest.fn((_val) => ok('noop')) 215 | 216 | teed.andThen(nextFn) 217 | 218 | expect(nextFn).not.toHaveBeenCalled() 219 | }) 220 | }) 221 | describe('orElse', () => { 222 | it('Skips orElse on an Ok value', () => { 223 | const okVal = ok(12) 224 | const errorCallback = vitest.fn((_errVal) => err('It is now a string')) 225 | 226 | expect(okVal.orElse(errorCallback)).toEqual(ok(12)) 227 | expect(errorCallback).not.toHaveBeenCalled() 228 | }) 229 | }) 230 | 231 | it('unwrapOr and return the Ok value', () => { 232 | const okVal = ok(12) 233 | expect(okVal.unwrapOr(1)).toEqual(12) 234 | }) 235 | 236 | it('Maps to a ResultAsync', async () => { 237 | const okVal = ok(12) 238 | 239 | const flattened = okVal.asyncAndThen((_number) => { 240 | // ... 241 | // complex async logic 242 | // ... 243 | return okAsync({ data: 'why not' }) 244 | }) 245 | 246 | expect(flattened).toBeInstanceOf(ResultAsync) 247 | 248 | const newResult = await flattened 249 | 250 | expect(newResult.isOk()).toBe(true) 251 | expect(newResult._unsafeUnwrap()).toStrictEqual({ data: 'why not' }) 252 | }) 253 | 254 | it('Maps to a promise', async () => { 255 | const asyncMapper = vitest.fn((_val) => { 256 | // ... 257 | // complex logic 258 | // .. 259 | 260 | // db queries 261 | // network calls 262 | // disk io 263 | // etc ... 264 | return Promise.resolve('Very Nice!') 265 | }) 266 | 267 | const okVal = ok(12) 268 | 269 | const promise = okVal.asyncMap(asyncMapper) 270 | 271 | expect(promise).toBeInstanceOf(ResultAsync) 272 | 273 | const newResult = await promise 274 | 275 | expect(newResult.isOk()).toBe(true) 276 | expect(asyncMapper).toHaveBeenCalledTimes(1) 277 | expect(newResult._unsafeUnwrap()).toStrictEqual('Very Nice!') 278 | }) 279 | 280 | it('Matches on an Ok', () => { 281 | const okMapper = vitest.fn((_val) => 'weeeeee') 282 | const errMapper = vitest.fn((_val) => 'wooooo') 283 | 284 | const matched = ok(12).match(okMapper, errMapper) 285 | 286 | expect(matched).toBe('weeeeee') 287 | expect(okMapper).toHaveBeenCalledTimes(1) 288 | expect(errMapper).not.toHaveBeenCalled() 289 | }) 290 | 291 | it('Unwraps without issue', () => { 292 | const okVal = ok(12) 293 | 294 | expect(okVal._unsafeUnwrap()).toBe(12) 295 | }) 296 | 297 | it('Can read the value after narrowing', () => { 298 | const fallible: () => Result = () => ok('safe to read') 299 | const val = fallible() 300 | 301 | // After this check we val is narrowed to Ok. Without this 302 | // line TypeScript will not allow accessing val.value. 303 | if (val.isErr()) return 304 | 305 | expect(val.value).toBe('safe to read') 306 | }) 307 | }) 308 | 309 | describe('Result.Err', () => { 310 | it('Creates an Err value', () => { 311 | const errVal = err('I have you now.') 312 | 313 | expect(errVal.isOk()).toBe(false) 314 | expect(errVal.isErr()).toBe(true) 315 | expect(errVal).toBeInstanceOf(Err) 316 | }) 317 | 318 | it('Is comparable', () => { 319 | expect(err(42)).toEqual(err(42)) 320 | expect(err(42)).not.toEqual(err(43)) 321 | }) 322 | 323 | it('Skips `map`', () => { 324 | const errVal = err('I am your father') 325 | 326 | const mapper = vitest.fn((_value) => 'noooo') 327 | 328 | const hopefullyNotMapped = errVal.map(mapper) 329 | 330 | expect(hopefullyNotMapped.isErr()).toBe(true) 331 | expect(mapper).not.toHaveBeenCalled() 332 | expect(hopefullyNotMapped._unsafeUnwrapErr()).toEqual(errVal._unsafeUnwrapErr()) 333 | }) 334 | 335 | it('Maps over an Err', () => { 336 | const errVal = err('Round 1, Fight!') 337 | 338 | const mapper = vitest.fn((error: string) => error.replace('1', '2')) 339 | 340 | const mapped = errVal.mapErr(mapper) 341 | 342 | expect(mapped.isErr()).toBe(true) 343 | expect(mapper).toHaveBeenCalledTimes(1) 344 | expect(mapped._unsafeUnwrapErr()).not.toEqual(errVal._unsafeUnwrapErr()) 345 | }) 346 | 347 | it('unwrapOr and return the default value', () => { 348 | const okVal = err('Oh nooo') 349 | expect(okVal.unwrapOr(1)).toEqual(1) 350 | }) 351 | 352 | it('Skips over andThen', () => { 353 | const errVal = err('Yolo') 354 | 355 | const mapper = vitest.fn((_val) => ok('yooyo')) 356 | 357 | const hopefullyNotFlattened = errVal.andThen(mapper) 358 | 359 | expect(hopefullyNotFlattened.isErr()).toBe(true) 360 | expect(mapper).not.toHaveBeenCalled() 361 | expect(errVal._unsafeUnwrapErr()).toEqual('Yolo') 362 | }) 363 | 364 | it('Skips over andThrough', () => { 365 | const errVal = err('Yolo') 366 | 367 | const mapper = vitest.fn((_val) => ok(undefined)) 368 | 369 | const hopefullyNotFlattened = errVal.andThrough(mapper) 370 | 371 | expect(hopefullyNotFlattened.isErr()).toBe(true) 372 | expect(mapper).not.toHaveBeenCalled() 373 | expect(errVal._unsafeUnwrapErr()).toEqual('Yolo') 374 | }) 375 | 376 | it('Skips over andTee', () => { 377 | const errVal = err('Yolo') 378 | 379 | const mapper = vitest.fn((_val) => {}) 380 | 381 | const hopefullyNotFlattened = errVal.andTee(mapper) 382 | 383 | expect(hopefullyNotFlattened.isErr()).toBe(true) 384 | expect(mapper).not.toHaveBeenCalled() 385 | expect(errVal._unsafeUnwrapErr()).toEqual('Yolo') 386 | }) 387 | 388 | it('Skips over asyncAndThrough but returns ResultAsync instead', async () => { 389 | const errVal = err('Yolo') 390 | 391 | const mapper = vitest.fn((_val) => okAsync('Async')) 392 | 393 | const hopefullyNotFlattened = errVal.asyncAndThrough(mapper) 394 | expect(hopefullyNotFlattened).toBeInstanceOf(ResultAsync) 395 | 396 | const result = await hopefullyNotFlattened 397 | expect(result.isErr()).toBe(true) 398 | expect(mapper).not.toHaveBeenCalled() 399 | expect(result._unsafeUnwrapErr()).toEqual('Yolo') 400 | }) 401 | 402 | it('Transforms error into ResultAsync within `asyncAndThen`', async () => { 403 | const errVal = err('Yolo') 404 | 405 | const asyncMapper = vitest.fn((_val) => okAsync('yooyo')) 406 | 407 | const hopefullyNotFlattened = errVal.asyncAndThen(asyncMapper) 408 | 409 | expect(hopefullyNotFlattened).toBeInstanceOf(ResultAsync) 410 | expect(asyncMapper).not.toHaveBeenCalled() 411 | 412 | const syncResult = await hopefullyNotFlattened 413 | expect(syncResult._unsafeUnwrapErr()).toEqual('Yolo') 414 | }) 415 | 416 | it('Does not invoke callback within `asyncMap`', async () => { 417 | const asyncMapper = vitest.fn((_val) => { 418 | // ... 419 | // complex logic 420 | // .. 421 | 422 | // db queries 423 | // network calls 424 | // disk io 425 | // etc ... 426 | return Promise.resolve('Very Nice!') 427 | }) 428 | 429 | const errVal = err('nooooooo') 430 | 431 | const promise = errVal.asyncMap(asyncMapper) 432 | 433 | expect(promise).toBeInstanceOf(ResultAsync) 434 | 435 | const sameResult = await promise 436 | 437 | expect(sameResult.isErr()).toBe(true) 438 | expect(asyncMapper).not.toHaveBeenCalled() 439 | expect(sameResult._unsafeUnwrapErr()).toEqual(errVal._unsafeUnwrapErr()) 440 | }) 441 | 442 | it('Matches on an Err', () => { 443 | const okMapper = vitest.fn((_val) => 'weeeeee') 444 | const errMapper = vitest.fn((_val) => 'wooooo') 445 | 446 | const matched = err(12).match(okMapper, errMapper) 447 | 448 | expect(matched).toBe('wooooo') 449 | expect(okMapper).not.toHaveBeenCalled() 450 | expect(errMapper).toHaveBeenCalledTimes(1) 451 | }) 452 | 453 | it('Throws when you unwrap an Err', () => { 454 | const errVal = err('woopsies') 455 | 456 | expect(() => { 457 | errVal._unsafeUnwrap() 458 | }).toThrowError() 459 | }) 460 | 461 | it('Unwraps without issue', () => { 462 | const okVal = err(12) 463 | 464 | expect(okVal._unsafeUnwrapErr()).toBe(12) 465 | }) 466 | 467 | describe('orElse', () => { 468 | it('invokes the orElse callback on an Err value', () => { 469 | const okVal = err('BOOOM!') 470 | const errorCallback = vitest.fn((_errVal) => err(true)) 471 | 472 | expect(okVal.orElse(errorCallback)).toEqual(err(true)) 473 | expect(errorCallback).toHaveBeenCalledTimes(1) 474 | }) 475 | }) 476 | }) 477 | 478 | describe('Result.fromThrowable', () => { 479 | it('Creates a function that returns an OK result when the inner function does not throw', () => { 480 | const hello = (): string => 'hello' 481 | const safeHello = Result.fromThrowable(hello) 482 | 483 | const result = hello() 484 | const safeResult = safeHello() 485 | 486 | expect(safeResult).toBeInstanceOf(Ok) 487 | expect(result).toEqual(safeResult._unsafeUnwrap()) 488 | }) 489 | 490 | // Added for issue #300 -- the test here is not so much that expectations are met as that the test compiles. 491 | it('Accepts an inner function which takes arguments', () => { 492 | const hello = (fname: string): string => `hello, ${fname}` 493 | const safeHello = Result.fromThrowable(hello) 494 | 495 | const result = hello('Dikembe') 496 | const safeResult = safeHello('Dikembe') 497 | 498 | expect(safeResult).toBeInstanceOf(Ok) 499 | expect(result).toEqual(safeResult._unsafeUnwrap()) 500 | }) 501 | 502 | it('Creates a function that returns an err when the inner function throws', () => { 503 | const thrower = (): string => { 504 | throw new Error() 505 | } 506 | 507 | // type: () => Result 508 | // received types from thrower fn, no errorFn is provides therefore Err type is unknown 509 | const safeThrower = Result.fromThrowable(thrower) 510 | const result = safeThrower() 511 | 512 | expect(result).toBeInstanceOf(Err) 513 | expect(result._unsafeUnwrapErr()).toBeInstanceOf(Error) 514 | }) 515 | 516 | it('Accepts an error handler as a second argument', () => { 517 | const thrower = (): string => { 518 | throw new Error() 519 | } 520 | type MessageObject = { message: string } 521 | const toMessageObject = (): MessageObject => ({ message: 'error' }) 522 | 523 | // type: () => Result 524 | // received types from thrower fn and errorFn return type 525 | const safeThrower = Result.fromThrowable(thrower, toMessageObject) 526 | const result = safeThrower() 527 | 528 | expect(result.isOk()).toBe(false) 529 | expect(result.isErr()).toBe(true) 530 | expect(result).toBeInstanceOf(Err) 531 | expect(result._unsafeUnwrapErr()).toEqual({ message: 'error' }) 532 | }) 533 | 534 | it('has a top level export', () => { 535 | expect(fromThrowable).toBe(Result.fromThrowable) 536 | }) 537 | }) 538 | 539 | describe('Utils', () => { 540 | describe('`Result.combine`', () => { 541 | describe('Synchronous `combine`', () => { 542 | it('Combines a list of results into an Ok value', () => { 543 | const resultList = [ok(123), ok(456), ok(789)] 544 | 545 | const result = Result.combine(resultList) 546 | 547 | expect(result.isOk()).toBe(true) 548 | expect(result._unsafeUnwrap()).toEqual([123, 456, 789]) 549 | }) 550 | 551 | it('Combines a list of results into an Err value', () => { 552 | const resultList: Result[] = [ 553 | ok(123), 554 | err('boooom!'), 555 | ok(456), 556 | err('ahhhhh!'), 557 | ] 558 | 559 | const result = Result.combine(resultList) 560 | 561 | expect(result.isErr()).toBe(true) 562 | expect(result._unsafeUnwrapErr()).toBe('boooom!') 563 | }) 564 | 565 | it('Combines heterogeneous lists', () => { 566 | type HeterogenousList = [ 567 | Result, 568 | Result, 569 | Result, 570 | ] 571 | 572 | const heterogenousList: HeterogenousList = [ok('Yooooo'), ok(123), ok(true)] 573 | 574 | type ExpecteResult = Result<[string, number, boolean], string | number | boolean> 575 | 576 | const result: ExpecteResult = Result.combine(heterogenousList) 577 | 578 | expect(result._unsafeUnwrap()).toEqual(['Yooooo', 123, true]) 579 | }) 580 | 581 | it('Does not destructure / concatenate arrays', () => { 582 | type HomogenousList = [Result, Result] 583 | 584 | const homogenousList: HomogenousList = [ok(['hello', 'world']), ok([1, 2, 3])] 585 | 586 | type ExpectedResult = Result<[string[], number[]], boolean | string> 587 | 588 | const result: ExpectedResult = Result.combine(homogenousList) 589 | 590 | expect(result._unsafeUnwrap()).toEqual([ 591 | ['hello', 'world'], 592 | [1, 2, 3], 593 | ]) 594 | }) 595 | }) 596 | 597 | describe('`ResultAsync.combine`', () => { 598 | it('Combines a list of async results into an Ok value', async () => { 599 | const asyncResultList = [okAsync(123), okAsync(456), okAsync(789)] 600 | 601 | const resultAsync: ResultAsync = ResultAsync.combine(asyncResultList) 602 | 603 | expect(resultAsync).toBeInstanceOf(ResultAsync) 604 | 605 | const result = await ResultAsync.combine(asyncResultList) 606 | 607 | expect(result.isOk()).toBe(true) 608 | expect(result._unsafeUnwrap()).toEqual([123, 456, 789]) 609 | }) 610 | 611 | it('Combines a list of results into an Err value', async () => { 612 | const resultList: ResultAsync[] = [ 613 | okAsync(123), 614 | errAsync('boooom!'), 615 | okAsync(456), 616 | errAsync('ahhhhh!'), 617 | ] 618 | 619 | const result = await ResultAsync.combine(resultList) 620 | 621 | expect(result.isErr()).toBe(true) 622 | expect(result._unsafeUnwrapErr()).toBe('boooom!') 623 | }) 624 | 625 | it('Combines heterogeneous lists', async () => { 626 | type HeterogenousList = [ 627 | ResultAsync, 628 | ResultAsync, 629 | ResultAsync, 630 | ResultAsync, 631 | ] 632 | 633 | const heterogenousList: HeterogenousList = [ 634 | okAsync('Yooooo'), 635 | okAsync(123), 636 | okAsync(true), 637 | okAsync([1, 2, 3]), 638 | ] 639 | 640 | type ExpecteResult = Result<[string, number, boolean, number[]], string | number | boolean> 641 | 642 | const result: ExpecteResult = await ResultAsync.combine(heterogenousList) 643 | 644 | expect(result._unsafeUnwrap()).toEqual(['Yooooo', 123, true, [1, 2, 3]]) 645 | }) 646 | }) 647 | }) 648 | describe('`Result.combineWithAllErrors`', () => { 649 | describe('Synchronous `combineWithAllErrors`', () => { 650 | it('Combines a list of results into an Ok value', () => { 651 | const resultList = [ok(123), ok(456), ok(789)] 652 | 653 | const result = Result.combineWithAllErrors(resultList) 654 | 655 | expect(result.isOk()).toBe(true) 656 | expect(result._unsafeUnwrap()).toEqual([123, 456, 789]) 657 | }) 658 | 659 | it('Combines a list of results into an Err value', () => { 660 | const resultList: Result[] = [ 661 | ok(123), 662 | err('boooom!'), 663 | ok(456), 664 | err('ahhhhh!'), 665 | ] 666 | 667 | const result = Result.combineWithAllErrors(resultList) 668 | 669 | expect(result.isErr()).toBe(true) 670 | expect(result._unsafeUnwrapErr()).toEqual(['boooom!', 'ahhhhh!']) 671 | }) 672 | 673 | it('Combines heterogeneous lists', () => { 674 | type HeterogenousList = [ 675 | Result, 676 | Result, 677 | Result, 678 | ] 679 | 680 | const heterogenousList: HeterogenousList = [ok('Yooooo'), ok(123), ok(true)] 681 | 682 | type ExpecteResult = Result<[string, number, boolean], (string | number | boolean)[]> 683 | 684 | const result: ExpecteResult = Result.combineWithAllErrors(heterogenousList) 685 | 686 | expect(result._unsafeUnwrap()).toEqual(['Yooooo', 123, true]) 687 | }) 688 | 689 | it('Does not destructure / concatenate arrays', () => { 690 | type HomogenousList = [Result, Result] 691 | 692 | const homogenousList: HomogenousList = [ok(['hello', 'world']), ok([1, 2, 3])] 693 | 694 | type ExpectedResult = Result<[string[], number[]], (boolean | string)[]> 695 | 696 | const result: ExpectedResult = Result.combineWithAllErrors(homogenousList) 697 | 698 | expect(result._unsafeUnwrap()).toEqual([ 699 | ['hello', 'world'], 700 | [1, 2, 3], 701 | ]) 702 | }) 703 | }) 704 | describe('`ResultAsync.combineWithAllErrors`', () => { 705 | it('Combines a list of async results into an Ok value', async () => { 706 | const asyncResultList = [okAsync(123), okAsync(456), okAsync(789)] 707 | 708 | const result = await ResultAsync.combineWithAllErrors(asyncResultList) 709 | 710 | expect(result.isOk()).toBe(true) 711 | expect(result._unsafeUnwrap()).toEqual([123, 456, 789]) 712 | }) 713 | 714 | it('Combines a list of results into an Err value', async () => { 715 | const asyncResultList: ResultAsync[] = [ 716 | okAsync(123), 717 | errAsync('boooom!'), 718 | okAsync(456), 719 | errAsync('ahhhhh!'), 720 | ] 721 | 722 | const result = await ResultAsync.combineWithAllErrors(asyncResultList) 723 | 724 | expect(result.isErr()).toBe(true) 725 | expect(result._unsafeUnwrapErr()).toEqual(['boooom!', 'ahhhhh!']) 726 | }) 727 | 728 | it('Combines heterogeneous lists', async () => { 729 | type HeterogenousList = [ 730 | ResultAsync, 731 | ResultAsync, 732 | ResultAsync, 733 | ] 734 | 735 | const heterogenousList: HeterogenousList = [okAsync('Yooooo'), okAsync(123), okAsync(true)] 736 | 737 | type ExpecteResult = Result<[string, number, boolean], (string | number | boolean)[]> 738 | 739 | const result: ExpecteResult = await ResultAsync.combineWithAllErrors(heterogenousList) 740 | 741 | expect(result._unsafeUnwrap()).toEqual(['Yooooo', 123, true]) 742 | }) 743 | }) 744 | 745 | describe('testdouble `ResultAsync.combine`', () => { 746 | interface ITestInterface { 747 | getName(): string 748 | setName(name: string): void 749 | getAsyncResult(): ResultAsync 750 | } 751 | 752 | it('Combines `testdouble` proxies from mocks generated via interfaces', async () => { 753 | const mock = td.object() 754 | 755 | const result = await ResultAsync.combine([okAsync(mock)] as const) 756 | 757 | expect(result).toBeDefined() 758 | expect(result.isErr()).toBeFalsy() 759 | const unwrappedResult = result._unsafeUnwrap() 760 | 761 | expect(unwrappedResult.length).toBe(1) 762 | expect(unwrappedResult[0]).toBe(mock) 763 | }) 764 | }) 765 | }) 766 | }) 767 | 768 | describe('ResultAsync', () => { 769 | it('Is awaitable to a Result', async () => { 770 | // For a success value 771 | const asyncVal = okAsync(12) 772 | expect(asyncVal).toBeInstanceOf(ResultAsync) 773 | 774 | const val = await asyncVal 775 | 776 | expect(val).toBeInstanceOf(Ok) 777 | expect(val._unsafeUnwrap()).toEqual(12) 778 | 779 | // For an error 780 | const asyncErr = errAsync('Wrong format') 781 | expect(asyncErr).toBeInstanceOf(ResultAsync) 782 | 783 | const err = await asyncErr 784 | 785 | expect(err).toBeInstanceOf(Err) 786 | expect(err._unsafeUnwrapErr()).toEqual('Wrong format') 787 | }) 788 | 789 | describe('acting as a Promise', () => { 790 | it('Is chainable like any Promise', async () => { 791 | // For a success value 792 | const asyncValChained = okAsync(12).then((res) => { 793 | if (res.isOk()) { 794 | return res.value + 2 795 | } 796 | }) 797 | 798 | expect(asyncValChained).toBeInstanceOf(Promise) 799 | const val = await asyncValChained 800 | expect(val).toEqual(14) 801 | 802 | // For an error 803 | const asyncErrChained = errAsync('Oops').then((res) => { 804 | if (res.isErr()) { 805 | return res.error + '!' 806 | } 807 | }) 808 | 809 | expect(asyncErrChained).toBeInstanceOf(Promise) 810 | const err = await asyncErrChained 811 | expect(err).toEqual('Oops!') 812 | }) 813 | 814 | it('Can be used with Promise.all', async () => { 815 | const allResult = await Promise.all([okAsync('1')]) 816 | 817 | expect(allResult).toHaveLength(1) 818 | expect(allResult[0]).toBeInstanceOf(Ok) 819 | if (!(allResult[0] instanceof Ok)) return 820 | expect(allResult[0].isOk()).toBe(true) 821 | expect(allResult[0]._unsafeUnwrap()).toEqual('1') 822 | }) 823 | 824 | it('rejects if the underlying promise is rejected', () => { 825 | const asyncResult = new ResultAsync(Promise.reject('oops')) 826 | expect(asyncResult).rejects.toBe('oops') 827 | }) 828 | }) 829 | 830 | describe('map', () => { 831 | it('Maps a value using a synchronous function', async () => { 832 | const asyncVal = okAsync(12) 833 | 834 | const mapSyncFn = vitest.fn((number) => number.toString()) 835 | 836 | const mapped = asyncVal.map(mapSyncFn) 837 | 838 | expect(mapped).toBeInstanceOf(ResultAsync) 839 | 840 | const newVal = await mapped 841 | 842 | expect(newVal.isOk()).toBe(true) 843 | expect(newVal._unsafeUnwrap()).toBe('12') 844 | expect(mapSyncFn).toHaveBeenCalledTimes(1) 845 | }) 846 | 847 | it('Maps a value using an asynchronous function', async () => { 848 | const asyncVal = okAsync(12) 849 | 850 | const mapAsyncFn = vitest.fn((number) => Promise.resolve(number.toString())) 851 | 852 | const mapped = asyncVal.map(mapAsyncFn) 853 | 854 | expect(mapped).toBeInstanceOf(ResultAsync) 855 | 856 | const newVal = await mapped 857 | 858 | expect(newVal.isOk()).toBe(true) 859 | expect(newVal._unsafeUnwrap()).toBe('12') 860 | expect(mapAsyncFn).toHaveBeenCalledTimes(1) 861 | }) 862 | 863 | it('Skips an error', async () => { 864 | const asyncErr = errAsync('Wrong format') 865 | 866 | const mapSyncFn = vitest.fn((number) => number.toString()) 867 | 868 | const notMapped = asyncErr.map(mapSyncFn) 869 | 870 | expect(notMapped).toBeInstanceOf(ResultAsync) 871 | 872 | const newVal = await notMapped 873 | 874 | expect(newVal.isErr()).toBe(true) 875 | expect(newVal._unsafeUnwrapErr()).toBe('Wrong format') 876 | expect(mapSyncFn).toHaveBeenCalledTimes(0) 877 | }) 878 | }) 879 | 880 | describe('mapErr', () => { 881 | it('Maps an error using a synchronous function', async () => { 882 | const asyncErr = errAsync('Wrong format') 883 | 884 | const mapErrSyncFn = vitest.fn((str) => 'Error: ' + str) 885 | 886 | const mappedErr = asyncErr.mapErr(mapErrSyncFn) 887 | 888 | expect(mappedErr).toBeInstanceOf(ResultAsync) 889 | 890 | const newVal = await mappedErr 891 | 892 | expect(newVal.isErr()).toBe(true) 893 | expect(newVal._unsafeUnwrapErr()).toBe('Error: Wrong format') 894 | expect(mapErrSyncFn).toHaveBeenCalledTimes(1) 895 | }) 896 | 897 | it('Maps an error using an asynchronous function', async () => { 898 | const asyncErr = errAsync('Wrong format') 899 | 900 | const mapErrAsyncFn = vitest.fn((str) => Promise.resolve('Error: ' + str)) 901 | 902 | const mappedErr = asyncErr.mapErr(mapErrAsyncFn) 903 | 904 | expect(mappedErr).toBeInstanceOf(ResultAsync) 905 | 906 | const newVal = await mappedErr 907 | 908 | expect(newVal.isErr()).toBe(true) 909 | expect(newVal._unsafeUnwrapErr()).toBe('Error: Wrong format') 910 | expect(mapErrAsyncFn).toHaveBeenCalledTimes(1) 911 | }) 912 | 913 | it('Skips a value', async () => { 914 | const asyncVal = okAsync(12) 915 | 916 | const mapErrSyncFn = vitest.fn((str) => 'Error: ' + str) 917 | 918 | const notMapped = asyncVal.mapErr(mapErrSyncFn) 919 | 920 | expect(notMapped).toBeInstanceOf(ResultAsync) 921 | 922 | const newVal = await notMapped 923 | 924 | expect(newVal.isOk()).toBe(true) 925 | expect(newVal._unsafeUnwrap()).toBe(12) 926 | expect(mapErrSyncFn).toHaveBeenCalledTimes(0) 927 | }) 928 | }) 929 | 930 | describe('andThen', () => { 931 | it('Maps a value using a function returning a ResultAsync', async () => { 932 | const asyncVal = okAsync(12) 933 | 934 | const andThenResultAsyncFn = vitest.fn(() => okAsync('good')) 935 | 936 | const mapped = asyncVal.andThen(andThenResultAsyncFn) 937 | 938 | expect(mapped).toBeInstanceOf(ResultAsync) 939 | 940 | const newVal = await mapped 941 | 942 | expect(newVal.isOk()).toBe(true) 943 | expect(newVal._unsafeUnwrap()).toBe('good') 944 | expect(andThenResultAsyncFn).toHaveBeenCalledTimes(1) 945 | }) 946 | 947 | it('Maps a value using a function returning a Result', async () => { 948 | const asyncVal = okAsync(12) 949 | 950 | const andThenResultFn = vitest.fn(() => ok('good')) 951 | 952 | const mapped = asyncVal.andThen(andThenResultFn) 953 | 954 | expect(mapped).toBeInstanceOf(ResultAsync) 955 | 956 | const newVal = await mapped 957 | 958 | expect(newVal.isOk()).toBe(true) 959 | expect(newVal._unsafeUnwrap()).toBe('good') 960 | expect(andThenResultFn).toHaveBeenCalledTimes(1) 961 | }) 962 | 963 | it('Skips an Error', async () => { 964 | const asyncVal = errAsync('Wrong format') 965 | 966 | const andThenResultFn = vitest.fn(() => ok('good')) 967 | 968 | const notMapped = asyncVal.andThen(andThenResultFn) 969 | 970 | expect(notMapped).toBeInstanceOf(ResultAsync) 971 | 972 | const newVal = await notMapped 973 | 974 | expect(newVal.isErr()).toBe(true) 975 | expect(newVal._unsafeUnwrapErr()).toBe('Wrong format') 976 | expect(andThenResultFn).toHaveBeenCalledTimes(0) 977 | }) 978 | }) 979 | 980 | describe('andThrough', () => { 981 | it('Returns the original value when map function returning ResultAsync succeeds', async () => { 982 | const asyncVal = okAsync(12) 983 | /* 984 | A couple examples of this function 985 | 986 | DB persistence (create or update) 987 | API calls (create or update) 988 | */ 989 | const andThroughResultAsyncFn = vitest.fn(() => okAsync('good')) 990 | 991 | const thrued = asyncVal.andThrough(andThroughResultAsyncFn) 992 | 993 | expect(thrued).toBeInstanceOf(ResultAsync) 994 | 995 | const result = await thrued 996 | 997 | expect(result.isOk()).toBe(true) 998 | expect(result._unsafeUnwrap()).toBe(12) 999 | expect(andThroughResultAsyncFn).toHaveBeenCalledTimes(1) 1000 | }) 1001 | 1002 | it('Maps to an error when map function returning ResultAsync fails', async () => { 1003 | const asyncVal = okAsync(12) 1004 | 1005 | const andThroughResultAsyncFn = vitest.fn(() => errAsync('oh no!')) 1006 | 1007 | const thrued = asyncVal.andThrough(andThroughResultAsyncFn) 1008 | 1009 | expect(thrued).toBeInstanceOf(ResultAsync) 1010 | 1011 | const result = await thrued 1012 | 1013 | expect(result.isErr()).toBe(true) 1014 | expect(result._unsafeUnwrapErr()).toBe('oh no!') 1015 | expect(andThroughResultAsyncFn).toHaveBeenCalledTimes(1) 1016 | }) 1017 | 1018 | it('Returns the original value when map function returning Result succeeds', async () => { 1019 | const asyncVal = okAsync(12) 1020 | 1021 | const andThroughResultFn = vitest.fn(() => ok('good')) 1022 | 1023 | const thrued = asyncVal.andThrough(andThroughResultFn) 1024 | 1025 | expect(thrued).toBeInstanceOf(ResultAsync) 1026 | 1027 | const newVal = await thrued 1028 | 1029 | expect(newVal.isOk()).toBe(true) 1030 | expect(newVal._unsafeUnwrap()).toBe(12) 1031 | expect(andThroughResultFn).toHaveBeenCalledTimes(1) 1032 | }) 1033 | 1034 | it('Maps to an error when map function returning Result fails', async () => { 1035 | const asyncVal = okAsync(12) 1036 | 1037 | const andThroughResultFn = vitest.fn(() => err('oh no!')) 1038 | 1039 | const thrued = asyncVal.andThrough(andThroughResultFn) 1040 | 1041 | expect(thrued).toBeInstanceOf(ResultAsync) 1042 | 1043 | const newVal = await thrued 1044 | 1045 | expect(newVal.isErr()).toBe(true) 1046 | expect(newVal._unsafeUnwrapErr()).toBe('oh no!') 1047 | expect(andThroughResultFn).toHaveBeenCalledTimes(1) 1048 | }) 1049 | 1050 | it('Skips an Error', async () => { 1051 | const asyncVal = errAsync('Wrong format') 1052 | 1053 | const andThroughResultFn = vitest.fn(() => ok('good')) 1054 | 1055 | const notMapped = asyncVal.andThrough(andThroughResultFn) 1056 | 1057 | expect(notMapped).toBeInstanceOf(ResultAsync) 1058 | 1059 | const newVal = await notMapped 1060 | 1061 | expect(newVal.isErr()).toBe(true) 1062 | expect(newVal._unsafeUnwrapErr()).toBe('Wrong format') 1063 | expect(andThroughResultFn).toHaveBeenCalledTimes(0) 1064 | }) 1065 | }) 1066 | 1067 | describe('andTee', () => { 1068 | it('Calls the passed function but returns an original ok', async () => { 1069 | const okVal = okAsync(12) 1070 | const passedFn = vitest.fn((_number) => {}) 1071 | 1072 | const teed = await okVal.andTee(passedFn) 1073 | 1074 | expect(teed.isOk()).toBe(true) 1075 | expect(passedFn).toHaveBeenCalledTimes(1) 1076 | expect(teed._unsafeUnwrap()).toStrictEqual(12) 1077 | }) 1078 | it('returns an original ok even when the passed function fails', async () => { 1079 | const okVal = okAsync(12) 1080 | const passedFn = vitest.fn((_number) => { 1081 | throw new Error('OMG!') 1082 | }) 1083 | 1084 | const teed = await okVal.andTee(passedFn) 1085 | 1086 | expect(teed.isOk()).toBe(true) 1087 | expect(passedFn).toHaveBeenCalledTimes(1) 1088 | expect(teed._unsafeUnwrap()).toStrictEqual(12) 1089 | }) 1090 | }) 1091 | 1092 | describe('orTee', () => { 1093 | it('Calls the passed function but returns an original err', async () => { 1094 | const errVal = errAsync(12) 1095 | const passedFn = vitest.fn((_number) => {}) 1096 | 1097 | const teed = await errVal.orTee(passedFn) 1098 | 1099 | expect(teed.isErr()).toBe(true) 1100 | expect(passedFn).toHaveBeenCalledTimes(1) 1101 | expect(teed._unsafeUnwrapErr()).toStrictEqual(12) 1102 | }) 1103 | it('returns an original err even when the passed function fails', async () => { 1104 | const errVal = errAsync(12) 1105 | const passedFn = vitest.fn((_number) => { 1106 | throw new Error('OMG!') 1107 | }) 1108 | 1109 | const teed = await errVal.orTee(passedFn) 1110 | 1111 | expect(teed.isErr()).toBe(true) 1112 | expect(passedFn).toHaveBeenCalledTimes(1) 1113 | expect(teed._unsafeUnwrapErr()).toStrictEqual(12) 1114 | }) 1115 | }) 1116 | 1117 | describe('orElse', () => { 1118 | it('Skips orElse on an Ok value', async () => { 1119 | const okVal = okAsync(12) 1120 | const errorCallback = vitest.fn((_errVal) => errAsync('It is now a string')) 1121 | 1122 | const result = await okVal.orElse(errorCallback) 1123 | 1124 | expect(result).toEqual(ok(12)) 1125 | 1126 | expect(errorCallback).not.toHaveBeenCalled() 1127 | }) 1128 | 1129 | it('Invokes the orElse callback on an Err value', async () => { 1130 | const myResult = errAsync('BOOOM!') 1131 | const errorCallback = vitest.fn((_errVal) => errAsync(true)) 1132 | 1133 | const result = await myResult.orElse(errorCallback) 1134 | 1135 | expect(result).toEqual(err(true)) 1136 | expect(errorCallback).toHaveBeenCalledTimes(1) 1137 | }) 1138 | 1139 | it('Accepts a regular Result in the callback', async () => { 1140 | const myResult = errAsync('BOOOM!') 1141 | const errorCallback = vitest.fn((_errVal) => err(true)) 1142 | 1143 | const result = await myResult.orElse(errorCallback) 1144 | 1145 | expect(result).toEqual(err(true)) 1146 | expect(errorCallback).toHaveBeenCalledTimes(1) 1147 | }) 1148 | }) 1149 | 1150 | describe('match', () => { 1151 | it('Matches on an Ok', async () => { 1152 | const okMapper = vitest.fn((_val) => 'weeeeee') 1153 | const errMapper = vitest.fn((_val) => 'wooooo') 1154 | 1155 | const matched = await okAsync(12).match(okMapper, errMapper) 1156 | 1157 | expect(matched).toBe('weeeeee') 1158 | expect(okMapper).toHaveBeenCalledTimes(1) 1159 | expect(errMapper).not.toHaveBeenCalled() 1160 | }) 1161 | 1162 | it('Matches on an Error', async () => { 1163 | const okMapper = vitest.fn((_val) => 'weeeeee') 1164 | const errMapper = vitest.fn((_val) => 'wooooo') 1165 | 1166 | const matched = await errAsync('bad').match(okMapper, errMapper) 1167 | 1168 | expect(matched).toBe('wooooo') 1169 | expect(okMapper).not.toHaveBeenCalled() 1170 | expect(errMapper).toHaveBeenCalledTimes(1) 1171 | }) 1172 | }) 1173 | 1174 | describe('unwrapOr', () => { 1175 | it('returns a promise to the result value on an Ok', async () => { 1176 | const unwrapped = await okAsync(12).unwrapOr(10) 1177 | expect(unwrapped).toBe(12) 1178 | }) 1179 | 1180 | it('returns a promise to the provided default value on an Error', async () => { 1181 | const unwrapped = await errAsync(12).unwrapOr(10) 1182 | expect(unwrapped).toBe(10) 1183 | }) 1184 | }) 1185 | 1186 | describe('fromSafePromise', () => { 1187 | it('Creates a ResultAsync from a Promise', async () => { 1188 | const res = ResultAsync.fromSafePromise(Promise.resolve(12)) 1189 | 1190 | expect(res).toBeInstanceOf(ResultAsync) 1191 | 1192 | const val = await res 1193 | expect(val.isOk()).toBe(true) 1194 | expect(val._unsafeUnwrap()).toEqual(12) 1195 | }) 1196 | 1197 | it('has a top level export', () => { 1198 | expect(fromSafePromise).toBe(ResultAsync.fromSafePromise) 1199 | }) 1200 | }) 1201 | 1202 | describe('fromPromise', () => { 1203 | it('Accepts an error handler as a second argument', async () => { 1204 | const res = ResultAsync.fromPromise(Promise.reject('No!'), (e) => new Error('Oops: ' + e)) 1205 | 1206 | expect(res).toBeInstanceOf(ResultAsync) 1207 | 1208 | const val = await res 1209 | expect(val.isErr()).toBe(true) 1210 | expect(val._unsafeUnwrapErr()).toEqual(Error('Oops: No!')) 1211 | }) 1212 | 1213 | it('has a top level export', () => { 1214 | expect(fromPromise).toBe(ResultAsync.fromPromise) 1215 | }) 1216 | }) 1217 | 1218 | describe('ResultAsync.fromThrowable', () => { 1219 | it('creates a new function that returns a ResultAsync', async () => { 1220 | const example = ResultAsync.fromThrowable(async (a: number, b: number) => a + b) 1221 | const res = example(4, 8) 1222 | expect(res).toBeInstanceOf(ResultAsync) 1223 | 1224 | const val = await res 1225 | expect(val.isOk()).toBe(true) 1226 | expect(val._unsafeUnwrap()).toEqual(12) 1227 | }) 1228 | 1229 | it('handles synchronous errors', async () => { 1230 | const example = ResultAsync.fromThrowable(() => { 1231 | if (1 > 0) throw new Error('Oops: No!') 1232 | 1233 | return Promise.resolve(12) 1234 | }) 1235 | 1236 | const val = await example() 1237 | expect(val.isErr()).toBe(true) 1238 | 1239 | expect(val._unsafeUnwrapErr()).toEqual(Error('Oops: No!')) 1240 | }) 1241 | 1242 | it('handles asynchronous errors', async () => { 1243 | const example = ResultAsync.fromThrowable(async () => { 1244 | if (1 > 0) throw new Error('Oops: No!') 1245 | 1246 | return 12 1247 | }) 1248 | 1249 | const val = await example() 1250 | expect(val.isErr()).toBe(true) 1251 | 1252 | expect(val._unsafeUnwrapErr()).toEqual(Error('Oops: No!')) 1253 | }) 1254 | 1255 | it('Accepts an error handler as a second argument', async () => { 1256 | const example = ResultAsync.fromThrowable( 1257 | () => Promise.reject('No!'), 1258 | (e) => new Error('Oops: ' + e), 1259 | ) 1260 | 1261 | const val = await example() 1262 | expect(val.isErr()).toBe(true) 1263 | 1264 | expect(val._unsafeUnwrapErr()).toEqual(TypeError('Oops: No!')) 1265 | }) 1266 | 1267 | it('has a top level export', () => { 1268 | expect(fromAsyncThrowable).toBe(ResultAsync.fromThrowable) 1269 | }) 1270 | }) 1271 | 1272 | describe('okAsync', () => { 1273 | it('Creates a ResultAsync that resolves to an Ok', async () => { 1274 | const val = okAsync(12) 1275 | 1276 | expect(val).toBeInstanceOf(ResultAsync) 1277 | 1278 | const res = await val 1279 | 1280 | expect(res.isOk()).toBe(true) 1281 | expect(res._unsafeUnwrap()).toEqual(12) 1282 | }) 1283 | }) 1284 | 1285 | describe('errAsync', () => { 1286 | it('Creates a ResultAsync that resolves to an Err', async () => { 1287 | const err = errAsync('bad') 1288 | 1289 | expect(err).toBeInstanceOf(ResultAsync) 1290 | 1291 | const res = await err 1292 | 1293 | expect(res.isErr()).toBe(true) 1294 | expect(res._unsafeUnwrapErr()).toEqual('bad') 1295 | }) 1296 | }) 1297 | }) 1298 | -------------------------------------------------------------------------------- /tests/safe-try.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | safeTry, 3 | ok, 4 | okAsync, 5 | err, 6 | errAsync, 7 | Ok, 8 | Err, 9 | Result, 10 | ResultAsync, 11 | } from "../src" 12 | 13 | import { describe, expect, test } from 'vitest' 14 | 15 | describe('Returns what is returned from the generator function', () => { 16 | const val = "value" 17 | 18 | test("With synchronous Ok", () => { 19 | const res = safeTry(function*() { 20 | return ok(val) 21 | }) 22 | expect(res).toBeInstanceOf(Ok) 23 | expect(res._unsafeUnwrap()).toBe(val) 24 | }) 25 | 26 | test("With synchronous Err", () => { 27 | const res = safeTry(function*() { 28 | return err(val) 29 | }) 30 | expect(res).toBeInstanceOf(Err) 31 | expect(res._unsafeUnwrapErr()).toBe(val) 32 | }) 33 | 34 | test("With async Ok", async () => { 35 | const res = await safeTry(async function*() { 36 | return await okAsync(val) 37 | }) 38 | expect(res).toBeInstanceOf(Ok) 39 | expect(res._unsafeUnwrap()).toBe(val) 40 | }) 41 | 42 | test("With async Err", async () => { 43 | const res = await safeTry(async function*() { 44 | return await errAsync(val) 45 | }) 46 | expect(res).toBeInstanceOf(Err) 47 | expect(res._unsafeUnwrapErr()).toBe(val) 48 | }) 49 | }) 50 | 51 | describe("Returns the first occurence of Err instance as yiled*'s operand", () => { 52 | test("With synchronous results", () => { 53 | const errVal = "err" 54 | const okValues = Array() 55 | 56 | const result = safeTry(function*() { 57 | const okFoo = yield* ok("foo").safeUnwrap() 58 | okValues.push(okFoo) 59 | 60 | const okBar = yield* ok("bar").safeUnwrap() 61 | okValues.push(okBar) 62 | 63 | yield* err(errVal).safeUnwrap() 64 | 65 | throw new Error("This line should not be executed") 66 | }) 67 | 68 | expect(okValues).toMatchObject(["foo", "bar"]) 69 | 70 | expect(result).toBeInstanceOf(Err) 71 | expect(result._unsafeUnwrapErr()).toBe(errVal) 72 | }) 73 | 74 | test("With async results", async () => { 75 | const errVal = "err" 76 | const okValues = Array() 77 | 78 | const result = await safeTry(async function*() { 79 | const okFoo = yield* okAsync("foo").safeUnwrap() 80 | okValues.push(okFoo) 81 | 82 | const okBar = yield* okAsync("bar").safeUnwrap() 83 | okValues.push(okBar) 84 | 85 | yield* errAsync(errVal).safeUnwrap() 86 | 87 | throw new Error("This line should not be executed") 88 | }) 89 | 90 | expect(okValues).toMatchObject(["foo", "bar"]) 91 | 92 | expect(result).toBeInstanceOf(Err) 93 | expect(result._unsafeUnwrapErr()).toBe(errVal) 94 | }) 95 | 96 | test("Mix results of synchronous and async in AsyncGenerator", async () => { 97 | const errVal = "err" 98 | const okValues = Array() 99 | 100 | const result = await safeTry(async function*() { 101 | const okFoo = yield* okAsync("foo").safeUnwrap() 102 | okValues.push(okFoo) 103 | 104 | const okBar = yield* ok("bar").safeUnwrap() 105 | okValues.push(okBar) 106 | 107 | yield* err(errVal).safeUnwrap() 108 | 109 | throw new Error("This line should not be executed") 110 | }) 111 | 112 | expect(okValues).toMatchObject(["foo", "bar"]) 113 | 114 | expect(result).toBeInstanceOf(Err) 115 | expect(result._unsafeUnwrapErr()).toBe(errVal) 116 | }) 117 | }) 118 | 119 | describe("Tests if README's examples work", () => { 120 | const okValue = 3 121 | const errValue = "err!" 122 | function good(): Result { 123 | return ok(okValue) 124 | } 125 | function bad(): Result { 126 | return err(errValue) 127 | } 128 | function promiseGood(): Promise> { 129 | return Promise.resolve(ok(okValue)) 130 | } 131 | function promiseBad(): Promise> { 132 | return Promise.resolve(err(errValue)) 133 | } 134 | function asyncGood(): ResultAsync { 135 | return okAsync(okValue) 136 | } 137 | function asyncBad(): ResultAsync { 138 | return errAsync(errValue) 139 | } 140 | 141 | test("mayFail2 error", () => { 142 | function myFunc(): Result { 143 | return safeTry(function*() { 144 | return ok( 145 | (yield* good() 146 | .mapErr(e => `1st, ${e}`) 147 | .safeUnwrap()) 148 | + 149 | (yield* bad() 150 | .mapErr(e => `2nd, ${e}`) 151 | .safeUnwrap()) 152 | ) 153 | }) 154 | } 155 | 156 | const result = myFunc() 157 | expect(result.isErr()).toBe(true) 158 | expect(result._unsafeUnwrapErr()).toBe(`2nd, ${errValue}`) 159 | }) 160 | 161 | test("all ok", () => { 162 | function myFunc(): Result { 163 | return safeTry(function*() { 164 | return ok( 165 | (yield* good() 166 | .mapErr(e => `1st, ${e}`) 167 | .safeUnwrap()) 168 | + 169 | (yield* good() 170 | .mapErr(e => `2nd, ${e}`) 171 | .safeUnwrap()) 172 | ) 173 | }) 174 | } 175 | 176 | const result = myFunc() 177 | expect(result.isOk()).toBe(true) 178 | expect(result._unsafeUnwrap()).toBe(okValue + okValue) 179 | }) 180 | 181 | test("async mayFail1 error", async () => { 182 | function myFunc(): ResultAsync { 183 | return safeTry(async function*() { 184 | return ok( 185 | (yield* (await promiseBad()) 186 | .mapErr(e => `1st, ${e}`) 187 | .safeUnwrap()) 188 | + 189 | (yield* asyncGood() 190 | .mapErr(e => `2nd, ${e}`) 191 | .safeUnwrap()) 192 | ) 193 | }) 194 | } 195 | 196 | const result = await myFunc() 197 | expect(result.isErr()).toBe(true) 198 | expect(result._unsafeUnwrapErr()).toBe(`1st, ${errValue}`) 199 | }) 200 | 201 | test("async mayFail2 error", async () => { 202 | function myFunc(): ResultAsync { 203 | return safeTry(async function*() { 204 | return ok( 205 | (yield* (await promiseGood()) 206 | .mapErr(e => `1st, ${e}`) 207 | .safeUnwrap()) 208 | + 209 | (yield* asyncBad() 210 | .mapErr(e => `2nd, ${e}`) 211 | .safeUnwrap()) 212 | ) 213 | }) 214 | } 215 | 216 | const result = await myFunc() 217 | expect(result.isErr()).toBe(true) 218 | expect(result._unsafeUnwrapErr()).toBe(`2nd, ${errValue}`) 219 | }) 220 | 221 | test("promise async all ok", async () => { 222 | function myFunc(): ResultAsync { 223 | return safeTry(async function*() { 224 | return ok( 225 | (yield* (await promiseGood()) 226 | .mapErr(e => `1st, ${e}`) 227 | .safeUnwrap()) 228 | + 229 | (yield* asyncGood() 230 | .mapErr(e => `2nd, ${e}`) 231 | .safeUnwrap()) 232 | ) 233 | }) 234 | } 235 | 236 | const result = await myFunc() 237 | expect(result.isOk()).toBe(true) 238 | expect(result._unsafeUnwrap()).toBe(okValue + okValue) 239 | }) 240 | }) 241 | 242 | describe("it yields and works without safeUnwrap", () => { 243 | test("With synchronous Ok", () => { 244 | const res: Result = ok("ok"); 245 | 246 | const actual = safeTry(function* () { 247 | const x = yield* res; 248 | return ok(x); 249 | }); 250 | 251 | expect(actual).toBeInstanceOf(Ok); 252 | expect(actual._unsafeUnwrap()).toBe("ok"); 253 | }); 254 | 255 | test("With synchronous Err", () => { 256 | const res: Result = err("error"); 257 | 258 | const actual = safeTry(function* () { 259 | const x = yield* res; 260 | return ok(x); 261 | }); 262 | 263 | expect(actual).toBeInstanceOf(Err); 264 | expect(actual._unsafeUnwrapErr()).toBe("error"); 265 | }); 266 | 267 | const okValue = 3; 268 | const errValue = "err!"; 269 | 270 | function good(): Result { 271 | return ok(okValue); 272 | } 273 | function bad(): Result { 274 | return err(errValue); 275 | } 276 | function promiseGood(): Promise> { 277 | return Promise.resolve(ok(okValue)); 278 | } 279 | function promiseBad(): Promise> { 280 | return Promise.resolve(err(errValue)); 281 | } 282 | function asyncGood(): ResultAsync { 283 | return okAsync(okValue); 284 | } 285 | function asyncBad(): ResultAsync { 286 | return errAsync(errValue); 287 | } 288 | 289 | test("mayFail2 error", () => { 290 | function fn(): Result { 291 | return safeTry(function* () { 292 | const first = yield* good().mapErr((e) => `1st, ${e}`); 293 | const second = yield* bad().mapErr((e) => `2nd, ${e}`); 294 | 295 | return ok(first + second); 296 | }); 297 | } 298 | 299 | const result = fn(); 300 | expect(result.isErr()).toBe(true); 301 | expect(result._unsafeUnwrapErr()).toBe(`2nd, ${errValue}`); 302 | }); 303 | 304 | test("all ok", () => { 305 | function myFunc(): Result { 306 | return safeTry(function* () { 307 | const first = yield* good().mapErr((e) => `1st, ${e}`); 308 | const second = yield* good().mapErr((e) => `2nd, ${e}`); 309 | return ok(first + second); 310 | }); 311 | } 312 | 313 | const result = myFunc(); 314 | expect(result.isOk()).toBe(true); 315 | expect(result._unsafeUnwrap()).toBe(okValue + okValue); 316 | }); 317 | 318 | test("async mayFail1 error", async () => { 319 | function myFunc(): ResultAsync { 320 | return safeTry(async function* () { 321 | const first = yield* (await promiseBad()).mapErr((e) => `1st, ${e}`); 322 | const second = yield* asyncGood().mapErr((e) => `2nd, ${e}`); 323 | return ok(first + second); 324 | }); 325 | } 326 | 327 | const result = await myFunc(); 328 | expect(result.isErr()).toBe(true); 329 | expect(result._unsafeUnwrapErr()).toBe(`1st, ${errValue}`); 330 | }); 331 | 332 | test("async mayFail2 error", async () => { 333 | function myFunc(): ResultAsync { 334 | return safeTry(async function* () { 335 | const goodResult = await promiseGood(); 336 | const value = yield* goodResult.mapErr((e) => `1st, ${e}`); 337 | const value2 = yield* asyncBad().mapErr((e) => `2nd, ${e}`); 338 | 339 | return okAsync(value + value2); 340 | }); 341 | } 342 | 343 | const result = await myFunc(); 344 | expect(result.isErr()).toBe(true); 345 | expect(result._unsafeUnwrapErr()).toBe(`2nd, ${errValue}`); 346 | }); 347 | 348 | test("promise async all ok", async () => { 349 | function myFunc(): ResultAsync { 350 | return safeTry(async function* () { 351 | const first = yield* (await promiseGood()).mapErr((e) => `1st, ${e}`); 352 | const second = yield* asyncGood().mapErr((e) => `2nd, ${e}`); 353 | return ok(first + second); 354 | }); 355 | } 356 | 357 | const result = await myFunc(); 358 | expect(result.isOk()).toBe(true); 359 | expect(result._unsafeUnwrap()).toBe(okValue + okValue); 360 | }); 361 | }) 362 | -------------------------------------------------------------------------------- /tests/tsconfig.tests.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2016", 4 | "module": "ES2015", 5 | "noImplicitAny": true, 6 | "sourceMap": false, 7 | "downlevelIteration": true, 8 | "noUnusedLocals": false, 9 | "noUnusedParameters": false, 10 | "strictNullChecks": true, 11 | "strictFunctionTypes": true, 12 | "declaration": true, 13 | "moduleResolution": "Node", 14 | "baseUrl": "./src", 15 | "lib": [ 16 | "dom", 17 | "es2016", 18 | "es2017.object" 19 | ], 20 | "outDir": "dist", 21 | "skipLibCheck": true 22 | }, 23 | "include": [ 24 | "./index.test.ts", 25 | "./typecheck-tests.ts" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2015", 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "strict": false, 7 | "noImplicitAny": true, 8 | "sourceMap": false, 9 | "noUnusedLocals": true, 10 | "noUnusedParameters": true, 11 | "strictNullChecks": true, 12 | "strictFunctionTypes": true, 13 | "declaration": true, 14 | "baseUrl": "./src", 15 | "lib": [ 16 | "dom", 17 | "es2016", 18 | "es2017.object" 19 | ], 20 | "outDir": "dist", 21 | "skipLibCheck": true, 22 | "esModuleInterop": true 23 | }, 24 | "include": [ 25 | "src/**/*.ts" 26 | ], 27 | "exclude": [ 28 | "node_modules", 29 | "**/*.spec.ts" 30 | ] 31 | } 32 | --------------------------------------------------------------------------------