├── .github └── workflows │ ├── main.yml │ └── release.yml ├── .gitignore ├── .husky └── pre-commit ├── .vscode └── settings.json ├── assets └── typescript-result-logo.svg ├── biome.json ├── license.md ├── package-lock.json ├── package.json ├── readme.md ├── src ├── helpers.test.ts ├── helpers.ts ├── index.ts ├── integration.test.ts ├── result.test.ts └── result.ts ├── tsconfig.json ├── tsup.config.ts └── vitest.config.ts /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Main workflow 2 | on: 3 | push: 4 | branches: 5 | - master 6 | - next 7 | 8 | concurrency: 9 | cancel-in-progress: true 10 | group: merge-${{ github.ref }} 11 | 12 | jobs: 13 | BuildApp: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Git clone the repository 17 | uses: actions/checkout@v4 18 | 19 | - name: Setup NodeJS 20 | uses: actions/setup-node@v4 21 | with: 22 | node-version: 20 23 | cache: 'npm' 24 | 25 | - name: Install dependencies 26 | run: npm ci 27 | 28 | - name: Lint 29 | run: npm run lint 30 | 31 | - name: Typecheck 32 | run: npm run typecheck 33 | 34 | - name: Test 35 | run: npm run test:ci 36 | 37 | - name: Coverage 38 | run: npm run coverage 39 | 40 | - name: Build 41 | run: npm run build 42 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | on: 3 | release: 4 | types: [created] 5 | jobs: 6 | release: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Checkout repo 10 | uses: actions/checkout@v4 11 | with: 12 | ref: ${{ github.event.release.target_commitish }} 13 | 14 | - name: Validate and extract release information 15 | id: release 16 | uses: manovotny/github-releases-for-automated-package-publishing-action@v2.0.1 17 | 18 | - name: Setup NodeJS 19 | uses: actions/setup-node@v4 20 | with: 21 | node-version: 20 22 | cache: 'npm' 23 | always-auth: true 24 | registry-url: 'https://registry.npmjs.org' 25 | 26 | - name: Install dependencies 27 | run: npm ci 28 | 29 | - name: Build 30 | run: npm run build 31 | 32 | - name: Publish version 33 | if: steps.release.outputs.tag == '' 34 | run: npm publish 35 | env: 36 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 37 | 38 | - name: Publish tagged version 39 | if: steps.release.outputs.tag != '' 40 | run: npm publish --tag ${{ steps.release.outputs.tag }} 41 | env: 42 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | dist 4 | coverage -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npm run lint 2 | npm run typecheck 3 | npm run test:ci 4 | npm run coverage 5 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.codeActionsOnSave": { 3 | "quickfix.biome": "explicit" 4 | }, 5 | "[typescript]": { 6 | "editor.defaultFormatter": "biomejs.biome" 7 | }, 8 | "[javascript]": { 9 | "editor.defaultFormatter": "biomejs.biome" 10 | }, 11 | "[json]": { 12 | "editor.defaultFormatter": "biomejs.biome" 13 | }, 14 | "[markdown]": { 15 | "editor.defaultFormatter": "biomejs.biome" 16 | }, 17 | "typescript.reportStyleChecksAsWarnings": false, 18 | "prettier.enable": false, 19 | "typescript.tsdk": "node_modules/typescript/lib" 20 | } 21 | -------------------------------------------------------------------------------- /assets/typescript-result-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/1.7.3/schema.json", 3 | "organizeImports": { 4 | "enabled": true 5 | }, 6 | "linter": { 7 | "enabled": true, 8 | "ignore": ["**/*.type-test.ts"], 9 | "rules": { 10 | "recommended": true, 11 | "suspicious": { 12 | "noExplicitAny": "off" 13 | } 14 | } 15 | }, 16 | "vcs": { 17 | "enabled": true, 18 | "clientKind": "git", 19 | "useIgnoreFile": true, 20 | "defaultBranch": "main" 21 | }, 22 | "files": { 23 | "include": ["src/**/*"] 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /license.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | ===================== 3 | 4 | Copyright © 2024 Erik Verweij 5 | 6 | Permission is hereby granted, free of charge, to any person 7 | obtaining a copy of this software and associated documentation 8 | files (the “Software”), to deal in the Software without 9 | restriction, including without limitation the rights to use, 10 | copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the 12 | Software is furnished to do so, subject to the following 13 | conditions: 14 | 15 | The above copyright notice and this permission notice shall be 16 | included in all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, 19 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 20 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 21 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 22 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 23 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 24 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 25 | OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": false, 3 | "name": "typescript-result", 4 | "version": "3.1.1", 5 | "description": "A Result type inspired by Rust and Kotlin that leverages TypeScript's powerful type system to simplify error handling and make your code more readable and maintainable.", 6 | "keywords": [ 7 | "result", 8 | "type", 9 | "TypeScript", 10 | "ts", 11 | "error", 12 | "error handling", 13 | "exception", 14 | "ok", 15 | "success", 16 | "failure" 17 | ], 18 | "author": { 19 | "name": "Erik Verweij", 20 | "url": "https://github.com/everweij" 21 | }, 22 | "repository": "github:everweij/typescript-result", 23 | "homepage": "https://github.com/everweij/typescript-result#readme", 24 | "bugs": { 25 | "url": "https://github.com/everweij/typescript-result/issues/new" 26 | }, 27 | "license": "MIT", 28 | "files": [ 29 | "dist" 30 | ], 31 | "main": "dist/index.js", 32 | "type": "module", 33 | "module": "dist/index.js", 34 | "types": "dist/index.d.ts", 35 | "engines": { 36 | "node": ">=18" 37 | }, 38 | "exports": { 39 | ".": { 40 | "import": { 41 | "types": "./dist/index.d.ts", 42 | "default": "./dist/index.js" 43 | }, 44 | "require": { 45 | "types": "./dist/index.d.cts", 46 | "default": "./dist/index.cjs" 47 | } 48 | } 49 | }, 50 | "scripts": { 51 | "test": "vitest", 52 | "test:ci": "vitest run", 53 | "coverage": "vitest run --coverage", 54 | "lint": "biome check --write src", 55 | "typecheck": "tsc --noEmit", 56 | "prepare": "husky", 57 | "build": "tsup", 58 | "gzip-size": "npx gzip-size-cli ./dist/index.js" 59 | }, 60 | "devDependencies": { 61 | "@biomejs/biome": "1.8.3", 62 | "@types/node": "^22.5.2", 63 | "@vitest/coverage-v8": "^2.0.5", 64 | "husky": "^9.1.5", 65 | "tsup": "^8.2.4", 66 | "typescript": "^5.5.4", 67 | "vitest": "^2.0.5" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | TypeScript Result logo 2 | 3 | # TypeScript Result 4 | 5 | [![NPM](https://img.shields.io/npm/v/typescript-result.svg)](https://www.npmjs.com/package/typescript-result) 6 | [![TYPESCRIPT](https://img.shields.io/badge/%3C%2F%3E-typescript-blue)](http://www.typescriptlang.org/) 7 | [![BUNDLEPHOBIA](https://badgen.net/bundlephobia/minzip/typescript-result)](https://bundlephobia.com/result?p=typescript-result) 8 | [![Weekly downloads](https://badgen.net/npm/dw/typescript-result)](https://badgen.net/npm/dw/typescript-result) 9 | 10 | A Result type inspired by Rust and Kotlin that leverages TypeScript's powerful type system to simplify error handling and make your code more readable and maintainable with full type safety. 11 | 12 | ## Table of contents 13 | 14 | - [Getting started](#getting-started) 15 | - [Why should you use a result type?](#why-should-you-use-a-result-type) 16 | - [Why should you use this library?](#why-should-you-use-this-library) 17 | - [Guide](#guide) 18 | - [A note on errors](#a-note-on-errors) 19 | - [Creating a result](#creating-a-result) 20 | - [Performing operations on a result](#performing-operations-on-a-result) 21 | - [Unwrapping a result](#unwrapping-a-result) 22 | - [Handling errors](#handling-errors) 23 | - [Async operations](#async-operations) 24 | - [Merging or combining results](#merging-or-combining-results) 25 | - [API Reference](#api-reference) 26 | 27 | ## Getting started 28 | 29 | ### Installation 30 | 31 | Install using your favorite package manager: 32 | 33 | ```bash 34 | npm install typescript-result 35 | ``` 36 | 37 | ### Requirements 38 | 39 | #### Typescript 40 | 41 | Technically Typescript with version `4.8.0` or higher should work, but we recommend using version >= `5` when possible. 42 | 43 | Also it is important that you have `strict` or `strictNullChecks` enabled in your `tsconfig.json`: 44 | 45 | ```json 46 | { 47 | "compilerOptions": { 48 | "strict": true 49 | } 50 | } 51 | ``` 52 | 53 | #### Node 54 | 55 | Tested with Node.js version `16` and higher. 56 | 57 | ### Example 58 | 59 | Reading a JSON config file and validating its contents: 60 | 61 | ```typescript 62 | import { Result } from "typescript-result"; 63 | import fs from "node:fs/promises"; 64 | 65 | class IOError extends Error { 66 | readonly type = "io-error"; 67 | } 68 | 69 | class ParseError extends Error { 70 | readonly type = "parse-error"; 71 | } 72 | 73 | class ValidationError extends Error { 74 | readonly type = "validation-error"; 75 | } 76 | 77 | function readFile(path: string) { 78 | return Result.try( 79 | () => fs.readFile(path, "utf-8"), 80 | (error) => new IOError(`Unable to read file '${path}'`, { cause: error }) 81 | ); 82 | } 83 | 84 | const isObject = (value: unknown): value is Record => 85 | typeof value === "object" && value !== null; 86 | 87 | const isString = (value: unknown): value is string => typeof value === "string"; 88 | 89 | function getConfig(value: unknown) { 90 | if (!isObject(value)) { 91 | return Result.error(new ValidationError("Invalid config file")); 92 | } 93 | if (!value.name || !isString(value.name)) { 94 | return Result.error(new ValidationError("Missing or invalid 'name' field")); 95 | } 96 | if (!value.version || !isString(value.version)) { 97 | return Result.error( 98 | new ValidationError("Missing or invalid 'version' field") 99 | ); 100 | } 101 | 102 | return Result.ok({ name: value.name, version: value.version }); 103 | } 104 | 105 | const message = await readFile("./config.json") 106 | .mapCatching( 107 | (contents) => JSON.parse(contents), 108 | (error) => new ParseError("Unable to parse JSON", { cause: error }) 109 | ) 110 | .map((json) => getConfig(json)) 111 | .fold( 112 | (config) => 113 | `Successfully read config: name => ${config.name}, version => ${config.version}`, 114 | 115 | (error) => { 116 | switch (error.type) { 117 | case "io-error": 118 | return "Please check if the config file exists and is readable"; 119 | case "parse-error": 120 | return "Please check if the config file contains valid JSON"; 121 | case "validation-error": 122 | return error.message; 123 | } 124 | } 125 | ); 126 | ``` 127 | 128 | There's also an example repository available [here](https://github.com/everweij/typescript-result-example) that demonstrates how you could potentially use this library in the context of a web API. 129 | 130 | ## Why should you use a result type? 131 | 132 | ### Errors as values 133 | 134 | The Result type is a product of the ‘error-as-value’ movement, which in turn has its roots in functional programming. When throwing exceptions, all errors are treated equally and behave differently compared to the normal flow of the program. Instead, we like to make a distinction between expected errors and unexpected errors, and make the expected errors part of the normal flow of the program. By explicitly defining that a piece of code can either fail or succeed using the Result type, we can leverage TypeScript's powerful type system to keep track of everything that can go wrong in our code, and let it correct us when we overlook certain scenarios by performing exhaustive checks. This makes our code more type-safe, easier to maintain, and more transparent. 135 | 136 | ### Ergonomic error handling 137 | 138 | The goal is to keep the effort in using this library as light as possible, with a relatively small API surface. We don't want to introduce a whole new programming model where you would have to learn a ton of new concepts. Instead, we want to build on top of the existing features and best practices of the language, and provide a simple and intuitive API that is easy to understand and use. It also should be easy to incrementally adopt within existing codebase. 139 | 140 | ## Why should you use this library? 141 | 142 | There are already a few quality libraries out there that provide a Result type or similar for TypeScript. We believe that there are two reasons why you should consider using this library. 143 | 144 | ### Async support 145 | 146 | Result instances that are wrapped in a Promise can be painful to work with, because you would have to `await` every async operation before you can _chain_ next operations (like 'map', 'fold', etc.). To solve this and to make your code more ergonomic we provide an `AsyncResult` that is essentially a regular Promise containing a `Result` type, along with a couple of methods to make it easier to chain operations without having to assign the intermediate results to a variable or having to use `await` for each async operation. 147 | 148 | So instead of writing: 149 | 150 | ```typescript 151 | const firstAsyncResult = await someAsyncFunction1(); 152 | if (firstAsyncResult.isOk()) { 153 | const secondAsyncResult = await someAsyncFunction2(firstAsyncResult.value); 154 | if (secondAsyncResult.isOk()) { 155 | const thirdAsyncResult = await someAsyncFunction3(secondAsyncResult.value); 156 | if (thirdAsyncResult.isOk()) { 157 | // do something 158 | } else { 159 | // handle error 160 | } 161 | } else { 162 | // handle error 163 | } 164 | } else { 165 | // handle error 166 | } 167 | ``` 168 | 169 | You can write: 170 | 171 | ```typescript 172 | const result = await Result.fromAsync(someAsyncFunction1()) 173 | .map((value) => someAsyncFunction2(value)) 174 | .map((value) => someAsyncFunction3(value)) 175 | .fold( 176 | (value) => { 177 | // do something on success 178 | }, 179 | (error) => { 180 | // handle error 181 | } 182 | ); 183 | ``` 184 | 185 | You rarely have to deal with `AsyncResult` directly though, because this library will automatically convert the result of an async operation to an `AsyncResult` when needed, and since the API's are almost identical in shape, there's a big chance you wouldn't even notice you're using a `AsyncResult` under the hood. Let's look at an example what this means in practice: 186 | 187 | ```typescript 188 | // start with a sync value -> Result 189 | const result = await Result.ok(12) 190 | // map the value to a Promise -> AsyncResult 191 | .map((value) => Promise.resolve(value * 2)) // 192 | // map async to another result -> AsyncResult 193 | .map(async (value) => { 194 | if (value < 10) { 195 | return Result.error(new ValidationError("Value is too low")); 196 | } 197 | 198 | return Result.ok("All good!"); 199 | }) 200 | // unwrap the result -> Promise; 201 | .getOrElse((error) => error.message); 202 | ``` 203 | 204 | ### _Full_ type safety without a lot of boilerplate 205 | 206 | This library is able to track all possible outcomes simply by using type inference. Of course, there are edge cases, but most of the time all you have to do is to simply return `Result.ok()` or `Result.error()`, and the library will do the rest for you. 207 | In the example below, Typescript will complain that not all code paths return a value. Rightfully so, because we forgot to implement the case where there is not enough stock: 208 | 209 | ```typescript 210 | class NotEnoughStockError extends Error { 211 | readonly type = "not-enough-stock"; 212 | } 213 | 214 | class InsufficientBalanceError extends Error { 215 | readonly type = "insufficient-balance"; 216 | } 217 | 218 | function order(basket: Basket, stock: Stock, account: Account) { 219 | if (basket.getTotalPrice() > account.balance) { 220 | return Result.error(new InsufficientBalanceError()); 221 | } 222 | 223 | if (!stock.hasEnoughStock(basket.getProducts())) { 224 | return Result.error(new NotEnoughStockError()); 225 | } 226 | 227 | const order: Order = { /* skipped for brevity */ } 228 | 229 | return Result.ok(order); 230 | } 231 | 232 | function handleOrder(products: Product[], userId: number) { 233 | /* skipped for brevity */ 234 | 235 | return order(basket, stock, account).fold( 236 | () => ({ 237 | status: 200, 238 | body: "Order placed successfully", 239 | }), 240 | (error) => { // TS-Error: Not all code paths return a value 241 | switch(error.type) { 242 | case "insufficient-balance": 243 | return { 244 | status: 400, 245 | body: "Insufficient balance", 246 | } 247 | } 248 | } 249 | ); 250 | } 251 | ``` 252 | 253 | ## Guide 254 | 255 | ### A note on errors 256 | 257 | Errors are a fundamental part of the Result type. This library does not have a strong opinion on what your errors should look like; they can be any value, like a string, number, object, etc. Usually though, people tend to use instances of the `Error` class or any custom errors by subclassing the `Error` class. 258 | 259 | There's only one thing to keep in mind when it comes to using custom errors that extends the `Error` class: in certain circumstances, like inferring errors of a result type, TypeScript tends to unify types that look similar. This means that in the example below, TypeScript will infer the error type of the result to be `Error` instead of `ErrorA | ErrorB`. This is because TypeScript does not have a way to distinguish between the two errors, since they are both instances of the `Error` class. 260 | 261 | ```typescript 262 | class ErrorA extends Error {} 263 | class ErrorB extends Error {} 264 | 265 | function example() { 266 | if (condition) { 267 | return Result.error(new ErrorA()); 268 | } 269 | 270 | return Result.error(new ErrorB()); 271 | } 272 | 273 | const result = example(); 274 | if (result.isError()) { 275 | // TypeScript infers that result.error is of type Error, and not ErrorA | ErrorB 276 | console.error(result.error); 277 | } 278 | ``` 279 | 280 | To mitigate this, you can add a property on your custom errors, a so-called discriminant field, that makes it easier for TypeScript to distinguish between the different error types. In the example below, TypeScript will infer the error type of the result to be `ErrorA | ErrorB`: 281 | 282 | ```typescript 283 | class ErrorA extends Error { 284 | readonly type = "error-a"; 285 | } 286 | class ErrorB extends Error { 287 | readonly type = "error-b"; 288 | } 289 | 290 | function example() { 291 | if (condition) { 292 | return Result.error(new ErrorA()); 293 | } 294 | 295 | return Result.error(new ErrorB()); 296 | } 297 | 298 | const result = example(); 299 | if (result.isError()) { 300 | console.error(result.error); // ErrorA | ErrorB 301 | } 302 | ``` 303 | 304 | Although we agree that this might be a bit cumbersome, it is a small price to pay for the benefits that you get in return. For consistency, we recommend to always add a `readonly type` property to your custom errors. 305 | 306 | ### Creating a result 307 | 308 | #### Basic usage 309 | 310 | The most common way to create a result is by using the [`Result.ok`](#resultokvalue) and [`Result.error`](#resulterrorerror) static methods. 311 | 312 | The example below produces a result which contains either the outcome of the division or a `DivisionByZeroError` error. 313 | 314 | ```ts 315 | function divide(a: number, b: number) { 316 | if (b === 0) { 317 | return Result.error(new DivisionByZeroError(`Cannot divide ${a} by zero`)); 318 | } 319 | 320 | return Result.ok(a / b); 321 | } 322 | ``` 323 | 324 | Note that we didn't specify an explicit return type for the `divide` function. In most cases TypeScript is smart enough to infer the result types most of the times for you. In case of the example above, the return type gets inferred to `Result`. There are good reasons to specify the return type explicitly (e.g. clarity, readability, etc.), but in general it is not technically a necessity and therefore up to you to decide to define your returns explicit or not. 325 | 326 | Also note that when using `Result.ok` it is optional to provide a value, simply because not all operations produce a value. 327 | 328 | ```ts 329 | // this is fine 330 | const result = Result.ok(); // Result 331 | ``` 332 | 333 | #### Using `Result.try` and `Result.wrap` 334 | 335 | [`Result.try`](#resulttryfn-transform) is a convenient way to wrap code that might throw an error. The method will catch any exceptions that might be thrown and encapsulate them in a failed result. This is especially useful when you want to work with existing or external functions that might throw exceptions. You can often replace traditional try-catch blocks by wrapping the code in `Result.try`: 336 | 337 | ```ts 338 | // Using try-catch 339 | let result: Result; 340 | try { 341 | fs.writeFileSync("file.txt", "Hello, World!", "utf-8"); 342 | result = Result.ok(); 343 | } catch (error) { 344 | result = Result.error(error); 345 | } 346 | 347 | // Using Result.try 348 | const result = Result.try(() => fs.writeFileSync("file.txt", "Hello, World!", "utf-8")); 349 | ``` 350 | 351 | Here, we are using Node's `fs` module to write a file to disk. The `writeFileSync` method might throw an error if something goes wrong. You might not have the correct permissions for instance, or you ran out of disk space. By using `Result.try`, we can catch the error and encapsulate it in a failed result. 352 | 353 | Optionally, you can provide a second argument to `Result.try` which is a callback that allows you to transform the caught error into a more meaningful error. This is useful when you want to provide more context or when you want to wrap the error in a custom error type. 354 | 355 | ```ts 356 | const result = Result.try( 357 | () => fs.writeFileSync("file.txt", "Hello, World!", "utf-8"), 358 | (error) => new IOError("Failed to save file", { cause: error }), 359 | ); 360 | ``` 361 | 362 | Additionally, you can use [`Result.wrap`](#resultwrapfn) to wrap a function and return a new function that returns a result. The main difference compared to `Result.try` is that `Result.wrap` returns a function, while `Result.try` executes the function immediately. 363 | 364 | ```ts 365 | const safeWriteFile = Result.wrap(fs.writeFileSync); 366 | 367 | const result = safeWriteFile("file.txt", "Hello, World!", "utf-8"); // Result 368 | ``` 369 | 370 | ### Performing operations on a result 371 | 372 | Having a result is one thing, but in many cases, you also want to do something with it. This library provides a set of methods that lets you interact with the instance of a result in various ways. 373 | 374 | #### Chaining operations 375 | 376 | Similar to arrays and promises, you can also chain operations on a result. The main benefit of chaining operations is that you can keep your code compact, concise and readable, without having to assign intermediate results to variables. Let's look at an example: 377 | 378 | ```ts 379 | // Without chaining 380 | const resultA = someOperation(); 381 | if (resultA.isOk()) { 382 | const resultB = anotherOperation(resultA.value); 383 | if (resultB.isOk()) { 384 | const resultC = yetAnotherOperation(resultB.value); 385 | if (resultC.isOk()) { 386 | // do something 387 | } else { 388 | // handle error 389 | } 390 | } else { 391 | // handle error 392 | } 393 | } else { 394 | // handle error 395 | } 396 | 397 | // With chaining 398 | const result = someOperation() 399 | .map((value) => anotherOperation(value)) 400 | .map((value) => yetAnotherOperation(value)) 401 | 402 | if (result.isOk()) { 403 | // do something 404 | } else { 405 | // handle error 406 | } 407 | ``` 408 | 409 | The chained version is more concise and makes it easier to follow the flow of the program. Moreover, it allows us to _centralize_ error handling at the end of the flow. This is possible because all transformative operations produce new results which carry over any errors that might have occurred earlier in the chain. 410 | 411 | #### Transform: `map`, `mapCatching`, `recover`, `recoverCatching`, `mapError` 412 | 413 | Both [`map`](#maptransformfn) and [`recover`](#recoveronfailure) behave very similar in the sense that they transform a result using function provided by the user into a new result. The main difference is that `map` is used to transform a successful result, while `recover` is used to transform a failed result. 414 | 415 | The difference between the 'catching' variants is that they catch any exceptions that might be thrown inside the transformation function and encapsulate them in a failed result. So why would you not always use the 'catching' variants? It might be useful to make a distinction between exceptions that are expected and unexpected. If you _expect_ an exception to be thrown, like in the case of writing a file to disk, you might want to handle this use case. If you _don't expect_ an exception to be thrown, like in the case of saving something to a database, you might _not_ want to catch the exception and let the exception bubble up or even terminate the application. 416 | 417 | There's a subtle difference with `mapCatching` however. It takes an optional second argument which is a function that lets you transform any caught exception that was thrown inside the transformation function. This is useful when you want to provide more context or when you want to wrap the error in a custom error type. 418 | 419 | ```ts 420 | readFile("source.txt") 421 | .mapCatching( 422 | (contents) => writeFile("destination.txt", contents.toUpperCase()), 423 | (error) => new IOError("Failed to write file", { cause: error }) 424 | ) 425 | ``` 426 | 427 | Both `map` and `recover` are very flexible when it comes to the returning value of the transformation function. You can return a literal value, a new result, or even a promise that resolves to a value or a result. Other similar result-like libraries might have specific methods for each of thee use cases (e.g. `flatMap`, `chain`, etc.) and can be considered more strict. However, we like the approach of a smaller API surface with more flexibility. 428 | 429 | All transformations below produce the same type of result (`Result`, with the exception of the async transformations which produce an `AsyncResult`): 430 | ```ts 431 | someOperation() // Result 432 | .map((value) => value * 2) // Result 433 | .map((value) => Result.ok(value * 2)) // Result 434 | .map((value) => Promise.resolve(value * 2)) // AsyncResult; 435 | .map(async (value) => value * 2) // AsyncResult; 436 | .map(async (value) => Result.ok(value * 2)) // AsyncResult; 437 | ``` 438 | 439 | `recover` is especially useful when you want to fall back to another scenario when a previous operation fails. In the example below, we try to persist an item in the database. If that fails, we fall back to persisting the item locally. 440 | 441 | ```ts 442 | function persistInDB(item: Item): Result { 443 | // implementation 444 | }; 445 | function persistLocally(item: Item): Result { 446 | // implementation 447 | }; 448 | 449 | persistInDB(item).recover(() => persistLocally(item)); // Result 450 | ``` 451 | 452 | Note that after a recovery, any previous errors that might have occurred are _forgotten_. This is because when using `recover` you are essentially starting with a clean slate. In the example above we can assume that the `DbError` has been taken care of and therefore it has been removed from the final result. `IOError` on te other hand is still a possibility because it might occur after the recovery. 453 | 454 | Lastly, you can use `mapError` to transform the error of a failed result. This is especially useful when you want to transform the error into a different error type, or when you want to provide more context to the error: 455 | 456 | ```ts 457 | Result.try(() => fs.readFileSync("source.txt", "utf-8")) 458 | .mapCatching(contents => fs.writeFileSync("destination.txt", contents.toUpperCase(), "utf-8")) 459 | .mapError((error) => new IOError("Failed to transform file", { cause: error })); 460 | // Result 461 | ``` 462 | 463 | #### Side-effects: `onSuccess`, `onFailure` 464 | 465 | Sometimes you want to perform side-effects without modifying the result itself. This is where `onSuccess` and `onFailure` come in handy. Both methods allow you to run a callback function when the result is successful or when the result represents a failure. The main difference is that `onSuccess` is used for successful results, while `onFailure` is used for failed results. Both methods return the original instance of the result, so you can continue chaining other operations. 466 | 467 | In the example below, we log a message when an operation is successful and when it fails: 468 | 469 | ```ts 470 | someOperation() 471 | .onSuccess((value) => console.log("Operation succeeded with value", value)) 472 | .onFailure((error) => console.error("Operation failed with error", error)); 473 | ``` 474 | 475 | ### Unwrapping a result 476 | 477 | At some point in the flow of your program, you want to retrieve the value of a successful result or 478 | the error of a failed result. There are a couple of ways to do this, depending on your use case. 479 | 480 | #### Using `toTuple()` to destructure the result 481 | 482 | `toTuple()` returns the result in a tuple format where the first element is the _value_ and the second element is the _error_. We can leverage TypeScript's narrowing capabilities to infer the correct type of the value or error by doing a simple conditional check: 483 | 484 | ```ts 485 | declare const result: Result; 486 | 487 | const [value, error] = result.toTuple(); 488 | 489 | if (value) { 490 | // at this point the value must be a number 491 | } else { 492 | // error must be an instance of IOError 493 | } 494 | ``` 495 | 496 | Another approach is to return early when the result is a failure. This is a pattern common in the Go language: 497 | 498 | ```ts 499 | const [value, error] = result.toTuple(); 500 | 501 | if (error) { 502 | return Result.error(error); 503 | } 504 | 505 | return Result.ok(value * 2); 506 | ``` 507 | 508 | Note that in this example `Result.map` would be a better fit, but it illustrates the point. A more realistic example could be the handling of a request in a web API: 509 | 510 | ```ts 511 | function handleRoute(id: number) { 512 | const [value, error] = performOperation(id).toTuple(); 513 | 514 | if (error) { 515 | switch (error.type) { 516 | case "not-found": 517 | return { 518 | status: 404, 519 | body: "Not found", 520 | }; 521 | case "unauthorized": 522 | return { 523 | status: 401, 524 | body: "Unauthorized", 525 | }; 526 | default: 527 | return { 528 | status: 500, 529 | body: "Internal server error", 530 | }; 531 | } 532 | } 533 | 534 | return { 535 | status: 200, 536 | body: value, 537 | } 538 | } 539 | ``` 540 | 541 | #### Narrowing down the type using `isOk()` and `isError()` 542 | 543 | Another imperative approach is to use the `isOk()` and `isError()` methods to narrow down the type of the result: 544 | 545 | ```ts 546 | if (result.isOk()) { 547 | // TS infers that result.value is defined 548 | console.log(result.value); 549 | } else if (result.isError()) { 550 | // TS infers that result.error is defined 551 | console.error(result.error); 552 | } 553 | ``` 554 | 555 | If you do not use the type guards, TypeScript will infer the value or error as `T | undefined`. However, there is one exception to this rule: if a result has a error-type of `never`, it is safe to assume that the result can only be successful. Similarly, if the value-type is `never`, it is safe to assume that the result can only be a failure. 556 | 557 | ```ts 558 | const resultA = Result.ok(42); // Result 559 | resultA.value; // can only be a `number`, since the error-type is `never` 560 | 561 | const resultB = Result.error(new Error("Something went wrong")); // Result 562 | resultB.value; // can only by `undefined`, since the value-type is `never` 563 | resultB.error; // can only be an `Error`, since the value-type is `never` 564 | ``` 565 | 566 | #### Folding a result using `fold` 567 | 568 | The `fold` method is a more functional approach to unwrapping a result. It allows you to provide two callbacks: one for the successful case and one for the failure case. The `fold` method will execute the appropriate callback based on the outcome of the result. Using `fold` is especially useful when you want to return the a single 'thing' based on the outcome of the result, for example when you want to return a response object: 569 | 570 | ```ts 571 | function handleRoute(id: number) { 572 | return performOperation(id).fold( 573 | (value) => ({ 574 | status: 200, 575 | body: value, 576 | }), 577 | (error) => { 578 | switch (error.type) { 579 | case "not-found": 580 | return { 581 | status: 404, 582 | body: "Not found", 583 | }; 584 | case "unauthorized": 585 | return { 586 | status: 401, 587 | body: "Unauthorized", 588 | }; 589 | default: 590 | return { 591 | status: 500, 592 | body: "Internal server error", 593 | }; 594 | } 595 | } 596 | ); 597 | } 598 | ``` 599 | 600 | #### using 'getter' functions 601 | 602 | Please consult the [API Reference](#api-reference) for a full list of available methods: 603 | - [`errorOrNull`](#errorornull-1) 604 | - [`getOrNull`](#getornull-1) 605 | - [`getOrDefault`](#getordefaultdefaultvalue-1) 606 | - [`getOrElse`](#getorelseonfailure-1) 607 | 608 | ### Handling errors 609 | 610 | See the note on [errors](#a-note-on-errors) for more context. 611 | 612 | When using custom errors together with a `type` field to distinguish between different error types, you can use conditional checks like 'if-else' or `switch` statements to handle different error types. 613 | 614 | In order to perform exhaustive checks you can rely on the [`noImplicitReturns`](https://www.typescriptlang.org/tsconfig/#noImplicitReturns) compiler option when you are inside the context of a function and you are conditionally returning a value based on the `type` of the error: 615 | 616 | ```ts 617 | class ErrorA extends Error { 618 | readonly type = "error-a"; 619 | } 620 | 621 | class ErrorB extends Error { 622 | readonly type = "error-b"; 623 | } 624 | 625 | declare const result: Result; 626 | 627 | result.fold( 628 | (value) => /* do something */, 629 | (error) => { // TS-Error: Not all code paths return a value 630 | switch (error.type) { 631 | case "error-a": 632 | return /* something */; 633 | } 634 | } 635 | ) 636 | ``` 637 | 638 | Alternatively, you can manually perform exhaustive checks by checking for `never` using a `default` case in a `switch` statement, or the `else` branch in an `if-else` statement: 639 | 640 | ```ts 641 | class ErrorA extends Error { 642 | readonly type = "error-a"; 643 | } 644 | 645 | class ErrorB extends Error { 646 | readonly type = "error-b"; 647 | } 648 | 649 | declare const result: Result; 650 | 651 | if (result.isError()) { 652 | const error = result.error; 653 | if (error.type === "error-a") { 654 | // handle error-a 655 | } else if (error.type === "error-b") { 656 | // handle error-b 657 | } else { 658 | error satisfies never; 659 | } 660 | } 661 | ``` 662 | 663 | Because this pattern is so common, this library exposes a little utility function called `assertUnreachable`: 664 | 665 | ```ts 666 | import { assertUnreachable } from "typescript-result"; 667 | 668 | if (result.isError()) { 669 | const error = result.error; 670 | if (error.type === "error-a") { 671 | // handle error-a 672 | } else if (error.type === "error-b") { 673 | // handle error-b 674 | } else { 675 | assertUnreachable(error) 676 | } 677 | } 678 | ``` 679 | 680 | When not all code paths are considered, the `assertUnreachable` function will start to complain. At runtime it will also throw an error when the `default` case is reached. 681 | 682 | ### Async operations 683 | 684 | See [Async support](#async-support) for more context. 685 | 686 | Because it can be quite cumbersome to work with results that are wrapped in a promise, we provide an `AsyncResult` type that is essentially a regular promise that contains a `Result` type, along with most of the methods that are available on the regular `Result` type. This makes it easier to chain operations without having to assign the intermediate results to a variable or having to use `await` for each async operation. 687 | 688 | There are of course plenty of scenarios where an async function returns a `Result` (`Promise>`). In these cases, you can use the `fromAsync` and `fromAsyncCatching` methods to convert the promise to an `AsyncResult`, and continue chaining operations: 689 | 690 | ```ts 691 | async function someAsyncOperation(): Promise> { 692 | return Result.ok(42); 693 | } 694 | 695 | const result = await Result.fromAsync(someAsyncOperation()) 696 | .map((value) => value * 2) 697 | // etc... 698 | ``` 699 | 700 | ### Merging or combining results 701 | 702 | In some cases you might want to combine multiple results into a single result. This can be done using the [`Result.all`](#resultallitems) and [`Result.allCatching`](#resultallcatchingitems) methods. The `Result.all` method will return a successful result if all results are successful, otherwise it will return the first error that occurred. This is especially useful when you want to run multiple independent operations and bundle the outcome into a single result: 703 | 704 | ```ts 705 | declare function createTask(name: string): Result; 706 | 707 | const tasks = ["task-a", "task-b", "task-c"]; 708 | const result = Result.all(...tasks.map(createTask)); // Result 709 | ``` 710 | 711 | # API Reference 712 | 713 | ## Table of contents 714 | 715 | - [Result](#result) 716 | - Properties 717 | - [isResult](#isresult) 718 | - [value](#value) 719 | - [error](#error) 720 | - Instance methods 721 | - [isOk()](#isok) 722 | - [isError()](#iserror) 723 | - [toTuple()](#totuple) 724 | - [errorOrNull()](#errorornull) 725 | - [getOrNull()](#getornull) 726 | - [getOrDefault(defaultValue)](#getordefaultdefaultvalue) 727 | - [getOrElse(onFailure)](#getorelseonfailure) 728 | - [getOrThrow()](#getorthrow) 729 | - [fold(onSuccess, onFailure)](#foldonsuccess-onfailure) 730 | - [onFailure(action)](#onfailureaction) 731 | - [onSuccess(action)](#onsuccessaction) 732 | - [map(transformFn)](#maptransformfn) 733 | - [mapCatching(transformFn, transformErrorFn?)](#mapcatchingtransformfn-transformerrorfn) 734 | - [mapError(transformFn)](#maperrortransformfn) 735 | - [recover(onFailure)](#recoveronfailure) 736 | - [recoverCatching(onFailure)](#recovercatchingonfailure) 737 | - Static methods 738 | - [Result.ok(value)](#resultokvalue) 739 | - [Result.error(error)](#resulterrorerror) 740 | - [Result.isResult(possibleResult)](#resultisresultpossibleresult) 741 | - [Result.isAsyncResult(possibleAsyncResult)](#resultisasyncresultpossibleasyncresult) 742 | - [Result.all(items)](#resultallitems) 743 | - [Result.allCatching(items)](#resultallcatchingitems) 744 | - [Result.wrap(fn)](#resultwrapfn) 745 | - [Result.try(fn, [transform])](#resulttryfn-transform) 746 | - [Result.fromAsync(promise)](#resultfromasyncpromise) 747 | - [Result.fromAsyncCatching(promise)](#resultfromasynccatchingpromise) 748 | - [Result.assertOk(result)](#resultassertokresult) 749 | - [Result.assertError(result)](#resultasserterrorresult) 750 | - [AsyncResult](#asyncresult) 751 | - Properties 752 | - [isAsyncResult](#isasyncresult) 753 | - Instance methods 754 | - [toTuple()](#totuple-1) 755 | - [errorOrNull()](#errorornull-1) 756 | - [getOrNull()](#getornull-1) 757 | - [getOrDefault(defaultValue)](#getordefaultdefaultvalue-1) 758 | - [getOrElse(onFailure)](#getorelseonfailure-1) 759 | - [getOrThrow()](#getorthrow-1) 760 | - [fold(onSuccess, onFailure)](#foldonsuccess-onfailure-1) 761 | - [onFailure(action)](#onfailureaction-1) 762 | - [onSuccess(action)](#onsuccessaction-1) 763 | - [map(transformFn)](#maptransformfn-1) 764 | - [mapCatching(transformFn, transfornErrorFn?)](#mapcatchingtransformfn-transformerrorfn-1) 765 | - [mapError(transformFn)](#maperrortransformfn-1) 766 | - [recover(onFailure)](#recoveronfailure-1) 767 | - [recoverCatching(onFailure)](#recovercatchingonfailure-1) 768 | 769 | ## Result 770 | 771 | Represents the outcome of an operation that can either succeed or fail. 772 | 773 | ```ts 774 | class Result {} 775 | ``` 776 | 777 | ### isResult 778 | 779 | Utility getter that checks if the current instance is a `Result`. 780 | 781 | ### value 782 | 783 | Retrieves the encapsulated value of the result when the result is successful. 784 | 785 | > [!NOTE] 786 | > You can use [`Result.isOk()`](#isok) to narrow down the type to a successful result. 787 | 788 | #### Example 789 | obtaining the value of a result, without checking if it's successful 790 | ```ts 791 | declare const result: Result; 792 | 793 | result.value; // number | undefined 794 | ``` 795 | 796 | #### Example 797 | obtaining the value of a result, after checking for success 798 | ```ts 799 | declare const result: Result; 800 | 801 | if (result.isOk()) { 802 | result.value; // number 803 | } 804 | ``` 805 | 806 | ### error 807 | 808 | Retrieves the encapsulated error of the result when the result represents a failure. 809 | 810 | > [!NOTE] 811 | > You can use [`Result.isError()`](#iserror) to narrow down the type to a failed result. 812 | 813 | #### Example 814 | obtaining the value of a result, without checking if it's a failure 815 | 816 | ```ts 817 | declare const result: Result; 818 | 819 | result.error; // Error | undefined 820 | ``` 821 | 822 | #### Example 823 | obtaining the error of a result, after checking for failure 824 | ```ts 825 | declare const result: Result; 826 | 827 | if (result.isError()) { 828 | result.error; // Error 829 | } 830 | ``` 831 | 832 | ### isOk() 833 | 834 | Type guard that checks whether the result is successful. 835 | 836 | **returns** `true` if the result is successful, otherwise `false`. 837 | 838 | #### Example 839 | checking if a result is successful 840 | ```ts 841 | declare const result: Result; 842 | 843 | if (result.isOk()) { 844 | result.value; // number 845 | } 846 | ``` 847 | 848 | ### isError() 849 | 850 | Type guard that checks whether the result is successful. 851 | 852 | **returns** `true` if the result represents a failure, otherwise `false`. 853 | 854 | #### Example 855 | checking if a result represents a failure 856 | ```ts 857 | declare const result: Result; 858 | 859 | if (result.isError()) { 860 | result.error; // Error 861 | } 862 | ``` 863 | 864 | ### toTuple() 865 | 866 | **returns** the result in a tuple format where the first element is the value and the second element is the error. 867 | 868 | If the result is successful, the error will be `null`. If the result is a failure, the value will be `null`. 869 | This method is especially useful when you want to destructure the result into a tuple and use TypeScript's narrowing capabilities. 870 | 871 | #### Example 872 | Narrowing down the type using destructuring 873 | ```ts 874 | declare const result: Result; 875 | 876 | const [value, error] = result.toTuple(); 877 | 878 | if (error) { 879 | // error is ErrorA 880 | } else { 881 | // at this point the value must be a number 882 | } 883 | ``` 884 | 885 | ### errorOrNull() 886 | 887 | **returns** the encapsulated error if the result is a failure, otherwise `null`. 888 | 889 | ### getOrNull() 890 | 891 | **returns** the encapsulated value if the result is successful, otherwise `null`. 892 | 893 | ### getOrDefault(defaultValue) 894 | 895 | Retrieves the value of the result, or a default value if the result is a failure. 896 | 897 | #### Parameters 898 | 899 | - `defaultValue` The value to return if the result is a failure. 900 | 901 | **returns** The encapsulated value if the result is successful, otherwise the default value. 902 | 903 | #### Example 904 | obtaining the value of a result, or a default value 905 | ```ts 906 | declare const result: Result; 907 | 908 | const value = result.getOrDefault(0); // number 909 | ``` 910 | 911 | #### Example 912 | using a different type for the default value 913 | ```ts 914 | declare const result: Result; 915 | 916 | const value = result.getOrDefault("default"); // number | string 917 | ``` 918 | 919 | ### getOrElse(onFailure) 920 | 921 | Retrieves the value of the result, or transforms the error using the `onFailure` callback into a value. 922 | 923 | #### Parameters 924 | 925 | - `onFailure` callback function which allows you to transform the error into a value. The callback can be async as well. 926 | 927 | **returns** either the value if the result is successful, or the transformed error. 928 | 929 | #### Example 930 | transforming the error into a value 931 | ```ts 932 | declare const result: Result; 933 | 934 | const value = result.getOrElse((error) => 0); // number 935 | ``` 936 | 937 | #### Example 938 | using an async callback 939 | ```ts 940 | const value = await result.getOrElse(async (error) => 0); // Promise 941 | ``` 942 | 943 | ### getOrThrow() 944 | 945 | Retrieves the value of the result, or throws an error if the result is a failure. 946 | 947 | **returns** The value if the result is successful. 948 | 949 | **throws** the encapsulated error if the result is a failure. 950 | 951 | #### Example 952 | obtaining the value of a result, or throwing an error 953 | ```ts 954 | declare const result: Result; 955 | 956 | const value = result.getOrThrow(); // number 957 | ``` 958 | 959 | ### fold(onSuccess, onFailure) 960 | 961 | Returns the result of the `onSuccess` callback when the result represents success or 962 | the result of the `onFailure` callback when the result represents a failure. 963 | 964 | > [!NOTE] 965 | > Any exceptions that might be thrown inside the callbacks are not caught, so it is your responsibility 966 | > to handle these exceptions 967 | 968 | #### Parameters 969 | 970 | - `onSuccess` callback function to run when the result is successful. The callback can be async as well. 971 | - `onFailure` callback function to run when the result is a failure. The callback can be async as well. 972 | 973 | **returns** * the result of the callback that was executed. 974 | 975 | #### Example 976 | folding a result to a response-like object 977 | 978 | ```ts 979 | declare const result: Result; 980 | 981 | const response = result.fold( 982 | (user) => ({ status: 200, body: user }), 983 | (error) => { 984 | switch (error.type) { 985 | case "not-found": 986 | return { status: 404, body: "User not found" }; 987 | case "user-deactivated": 988 | return { status: 403, body: "User is deactivated" }; 989 | } 990 | } 991 | ); 992 | ``` 993 | 994 | ### onFailure(action) 995 | 996 | Calls the `action` callback when the result represents a failure. It is meant to be used for 997 | side-effects and the operation does not modify the result itself. 998 | 999 | #### Parameters 1000 | 1001 | - `action` callback function to run when the result is a failure. The callback can be async as well. 1002 | 1003 | **returns** the original instance of the result. 1004 | 1005 | > [!NOTE] 1006 | > Any exceptions that might be thrown inside the `action` callback are not caught, so it is your responsibility 1007 | > to handle these exceptions 1008 | 1009 | #### Example 1010 | adding logging between operations 1011 | ```ts 1012 | declare const result: Result; 1013 | 1014 | result 1015 | .onFailure((error) => console.error("I'm failing!", error)) 1016 | .map((value) => value 2); // proceed with other operations 1017 | ``` 1018 | 1019 | ### onSuccess(action) 1020 | 1021 | Calls the `action` callback when the result represents a success. It is meant to be used for 1022 | side-effects and the operation does not modify the result itself. 1023 | 1024 | #### Parameters 1025 | 1026 | - `action` callback function to run when the result is successful. The callback can be async as well. 1027 | 1028 | **returns** * the original instance of the result. If the callback is async, it returns a new [`AsyncResult`](#asyncresult) instance. 1029 | 1030 | > [!NOTE] 1031 | > Any exceptions that might be thrown inside the `action` callback are not caught, so it is your responsibility 1032 | > to handle these exceptions 1033 | 1034 | #### Example 1035 | adding logging between operations 1036 | ```ts 1037 | declare const result: Result; 1038 | 1039 | result 1040 | .onSuccess((value) => console.log("I'm a success!", value)) 1041 | .map((value) => value 2); // proceed with other operations 1042 | ``` 1043 | 1044 | #### Example 1045 | using an async callback 1046 | ```ts 1047 | declare const result: Result; 1048 | 1049 | const asyncResult = await result.onSuccess(async (value) => someAsyncOperation(value)); 1050 | ``` 1051 | 1052 | ### map(transformFn) 1053 | 1054 | Transforms the value of a successful result using the `transform` callback. 1055 | The `transform` callback can also return other `Result` or [`AsyncResult`](#asyncresult) instances, 1056 | which will be returned as-is (the `Error` types will be merged). 1057 | The operation will be ignored if the result represents a failure. 1058 | 1059 | #### Parameters 1060 | 1061 | - `transformFn` callback function to transform the value of the result. The callback can be async as well. 1062 | 1063 | **returns** * a new [`Result`](#result) instance with the transformed value, or a new [`AsyncResult`](#asyncresult) instance 1064 | if the `transformFn` function is async. 1065 | 1066 | > [!NOTE] 1067 | > Any exceptions that might be thrown inside the `transformFn` callback are not caught, so it is your responsibility 1068 | > to handle these exceptions. Please refer to [`Result.mapCatching()`](#mapcatchingtransformfn-transformerrorfn) for a version that catches exceptions 1069 | > and encapsulates them in a failed result. 1070 | 1071 | #### Example 1072 | transforming the value of a result 1073 | ```ts 1074 | declare const result: Result; 1075 | 1076 | const transformed = result.map((value) => value 2); // Result 1077 | ``` 1078 | 1079 | #### Example 1080 | returning a result instance 1081 | ```ts 1082 | declare const result: Result; 1083 | declare function multiplyByTwo(value: number): Result; 1084 | 1085 | const transformed = result.map((value) => multiplyByTwo(value)); // Result 1086 | ``` 1087 | 1088 | #### Example 1089 | doing an async transformation 1090 | ```ts 1091 | declare const result: Result; 1092 | 1093 | const transformed = result.map(async (value) => value 2); // AsyncResult 1094 | ``` 1095 | 1096 | #### Example 1097 | returning an async result instance 1098 | 1099 | ```ts 1100 | declare const result: Result; 1101 | declare function storeValue(value: number): AsyncResult; 1102 | 1103 | const transformed = result.map((value) => storeValue(value)); // AsyncResult 1104 | ``` 1105 | 1106 | ### mapCatching(transformFn, transformErrorFn?) 1107 | 1108 | Like [`Result.map`](#maptransformfn) it transforms the value of a successful result using the `transform` callback. 1109 | In addition, it catches any exceptions that might be thrown inside the `transform` callback and encapsulates them 1110 | in a failed result. 1111 | 1112 | #### Parameters 1113 | 1114 | - `transformFn` callback function to transform the value of the result. The callback can be async as well. 1115 | - `transformErrorFn` optional callback function that transforms any caught error inside `transformFn` into a specific error. 1116 | 1117 | **returns** * a new [`Result`](#result) instance with the transformed value, or a new [`AsyncResult`](#asyncresult) instance if the transform function is async. 1118 | 1119 | ### mapError(transformFn) 1120 | 1121 | Transforms the error of a failed result using the `transform` callback into a new error. 1122 | This can be useful when you want to transform the error into a different error type, or when you want to provide more context to the error. 1123 | 1124 | #### Parameters 1125 | 1126 | - `transformFn` callback function to transform the error of the result. 1127 | 1128 | **returns** a new failed [`Result`](#result) instance with the transformed error. 1129 | 1130 | #### Example 1131 | 1132 | transforming the error into a different error type 1133 | 1134 | ```ts 1135 | declare const result: Result; 1136 | 1137 | result.mapError((error) => new ErrorB(error.message)); // Result 1138 | ``` 1139 | 1140 | ### recover(onFailure) 1141 | 1142 | Transforms a failed result using the `onFailure` callback into a successful result. Useful for falling back to 1143 | other scenarios when a previous operation fails. 1144 | The `onFailure` callback can also return other `Result` or [`AsyncResult`](#asyncresult) instances, 1145 | which will be returned as-is. 1146 | After a recovery, logically, the result can only be a success. Therefore, the error type is set to `never`, unless 1147 | the `onFailure` callback returns a result-instance with another error type. 1148 | 1149 | #### Parameters 1150 | 1151 | - `onFailure` callback function to transform the error of the result. The callback can be async as well. 1152 | 1153 | **returns** a new successful [`Result`](#result) instance or a new successful [`AsyncResult`](#asyncresult) instance 1154 | when the result represents a failure, or the original instance if it represents a success. 1155 | 1156 | > [!NOTE] 1157 | > Any exceptions that might be thrown inside the `onFailure` callback are not caught, so it is your responsibility 1158 | > to handle these exceptions. Please refer to [`Result.recoverCatching`](#recovercatchingonfailure) for a version that catches exceptions 1159 | > and encapsulates them in a failed result. 1160 | 1161 | #### Example 1162 | transforming the error into a value 1163 | Note: Since we recover after trying to persist in the database, we can assume that the `DbError` has been taken care 1164 | of and therefore it has been removed from the final result. 1165 | ```ts 1166 | declare function persistInDB(item: Item): Result; 1167 | declare function persistLocally(item: Item): Result; 1168 | 1169 | persistInDB(item).recover(() => persistLocally(item)); // Result 1170 | ``` 1171 | 1172 | ### recoverCatching(onFailure) 1173 | 1174 | Like [`Result.recover`](#recoveronfailure) it transforms a failed result using the `onFailure` callback into a successful result. 1175 | In addition, it catches any exceptions that might be thrown inside the `onFailure` callback and encapsulates them 1176 | in a failed result. 1177 | 1178 | #### Parameters 1179 | 1180 | - `onFailure` callback function to transform the error of the result. The callback can be async as well. 1181 | 1182 | **returns** a new successful [`Result`](#result) instance or a new successful [`AsyncResult`](#asyncresult) instance when the result represents a failure, or the original instance if it represents a success. 1183 | 1184 | ### Result.ok(value) 1185 | 1186 | Creates a new result instance that represents a successful outcome. 1187 | 1188 | #### Parameters 1189 | 1190 | - `value` The value to encapsulate in the result. 1191 | 1192 | **returns** a new [`Result`](#result) instance. 1193 | 1194 | #### Example 1195 | ```ts 1196 | const result = Result.ok(42); // Result 1197 | ``` 1198 | 1199 | ### Result.error(error) 1200 | 1201 | Creates a new result instance that represents a failed outcome. 1202 | 1203 | #### Parameters 1204 | 1205 | - `error` The error to encapsulate in the result. 1206 | 1207 | **returns** a new [`Result`](#result) instance. 1208 | 1209 | #### Example 1210 | ```ts 1211 | const result = Result.error(new NotFoundError()); // Result 1212 | ``` 1213 | 1214 | ### Result.isResult(possibleResult) 1215 | 1216 | Type guard that checks whether the provided value is a [`Result`](#result) instance. 1217 | 1218 | #### Parameters 1219 | 1220 | - `possibleResult` any value that might be a [`Result`](#result) instance. 1221 | 1222 | **returns* `true` if the provided value is a [`Result`](#result) instance, otherwise `false`. 1223 | 1224 | ### Result.isAsyncResult(possibleAsyncResult) 1225 | 1226 | Type guard that checks whether the provided value is a [`AsyncResult`](#asyncresult) instance. 1227 | 1228 | #### Parameters 1229 | 1230 | - `possibleAsyncResult` any value that might be a [`AsyncResult`](#asyncresult) instance. 1231 | 1232 | **returns** `true` if the provided value is a [`AsyncResult`](#asyncresult) instance, otherwise `false`. 1233 | 1234 | ### Result.all(items) 1235 | 1236 | Similar to `Promise.all`, but for results. 1237 | Useful when you want to run multiple independent operations and bundle the outcome into a single result. 1238 | All possible values of the individual operations are collected into an array. `Result.all` will fail eagerly, 1239 | meaning that as soon as any of the operations fail, the entire result will be a failure. 1240 | Each argument can be a mixture of literal values, functions, [`Result`](#result) or [`AsyncResult`](#asyncresult) instances, or `Promise`. 1241 | 1242 | #### Parameters 1243 | 1244 | - `items` one or multiple literal value, function, [`Result`](#result) or [`AsyncResult`](#asyncresult) instance, or `Promise`. 1245 | 1246 | **returns** combined result of all the operations. 1247 | 1248 | > [!NOTE] 1249 | > Any exceptions that might be thrown are not caught, so it is your responsibility 1250 | > to handle these exceptions. Please refer to [`Result.allCatching`](#resultallcatchingitems) for a version that catches exceptions 1251 | > and encapsulates them in a failed result. 1252 | 1253 | #### Example 1254 | basic usage 1255 | ```ts 1256 | declare function createTask(name: string): Result; 1257 | 1258 | const tasks = ["task-a", "task-b", "task-c"]; 1259 | const result = Result.all(...tasks.map(createTask)); // Result 1260 | ``` 1261 | 1262 | #### Example 1263 | running multiple operations and combining the results 1264 | ```ts 1265 | const result = Result.all( 1266 | "a", 1267 | Promise.resolve("b"), 1268 | Result.ok("c"), 1269 | Result.try(async () => "d"), 1270 | () => "e", 1271 | () => Result.try(async () => "f"), 1272 | () => Result.ok("g"), 1273 | async () => "h", 1274 | ); // AsyncResult<[string, string, string, string, string, string, string, string], Error> 1275 | ``` 1276 | 1277 | ### Result.allCatching(items) 1278 | 1279 | Similar to [`Result.all`](#resultallitems), but catches any exceptions that might be thrown during the operations. 1280 | 1281 | #### Parameters 1282 | 1283 | - `items` one or multiple literal value, function, [`Result`](#result) or [`AsyncResult`](#asyncresult) instance, or `Promise`. 1284 | 1285 | **returns** combined result of all the operations. 1286 | 1287 | ### Result.wrap(fn) 1288 | 1289 | Wraps a function and returns a new function that returns a result. Especially useful when you want to work with 1290 | external functions that might throw exceptions. 1291 | The returned function will catch any exceptions that might be thrown and encapsulate them in a failed result. 1292 | 1293 | #### Parameters 1294 | 1295 | - `fn` function to wrap. Can be synchronous or asynchronous. 1296 | 1297 | **returns** a new function that returns a result. 1298 | 1299 | #### Example 1300 | basic usage 1301 | ```ts 1302 | declare function divide(a: number, b: number): number; 1303 | 1304 | const safeDivide = Result.wrap(divide); 1305 | const result = safeDivide(10, 0); // Result 1306 | ``` 1307 | 1308 | ### Result.try(fn, [transform]) 1309 | 1310 | Executes the given `fn` function and encapsulates the returned value as a successful result, or the 1311 | thrown exception as a failed result. In a way, you can view this method as a try-catch block that returns a result. 1312 | 1313 | #### Parameters 1314 | 1315 | - `fn` function with code to execute. Can be synchronous or asynchronous. 1316 | - `transform` optional callback to transform the caught error into a more meaningful error. 1317 | 1318 | **returns** a new [`Result`](#result) instance. 1319 | 1320 | #### Example 1321 | basic usage 1322 | ```ts 1323 | declare function saveFileToDisk(filename: string): void; // might throw an error 1324 | 1325 | const result = Result.try(() => saveFileToDisk("file.txt")); // Result 1326 | ``` 1327 | 1328 | #### Example 1329 | basic usage with error transformation 1330 | ```ts 1331 | declare function saveFileToDisk(filename: string): void; // might throw an error 1332 | 1333 | const result = Result.try( 1334 | () => saveFileToDisk("file.txt"), 1335 | (error) => new IOError("Failed to save file", { cause: error }) 1336 | ); // Result 1337 | ``` 1338 | 1339 | ### Result.fromAsync(promise) 1340 | 1341 | Utility method to transform a Promise, that holds a literal value or 1342 | a [`Result`](#result) or [`AsyncResult`](#asyncresult) instance, into an [`AsyncResult`](#asyncresult) instance. Useful when you want to immediately chain operations 1343 | after calling an async function. 1344 | 1345 | #### Parameters 1346 | 1347 | - `promise` a Promise that holds a literal value or a [`Result`](#result) or [`AsyncResult`](#asyncresult) instance. 1348 | 1349 | **returns** a new [`AsyncResult`](#asyncresult) instance. 1350 | 1351 | > [!NOTE] 1352 | > Any exceptions that might be thrown are not caught, so it is your responsibility 1353 | > to handle these exceptions. Please refer to [`Result.fromAsyncCatching`](#resultfromasynccatchingpromise) for a version that catches exceptions 1354 | > and encapsulates them in a failed result. 1355 | 1356 | #### Example 1357 | basic usage 1358 | 1359 | ```ts 1360 | declare function someAsyncOperation(): Promise>; 1361 | 1362 | // without 'Result.fromAsync' 1363 | const result = (await someAsyncOperation()).map((value) => value 2); // Result 1364 | 1365 | // with 'Result.fromAsync' 1366 | const asyncResult = Result.fromAsync(someAsyncOperation()).map((value) => value 2); // AsyncResult 1367 | ``` 1368 | 1369 | ### Result.fromAsyncCatching(promise) 1370 | 1371 | Similar to [`Result.fromAsync`](#resultfromasyncpromise) this method transforms a Promise into an [`AsyncResult`](#asyncresult) instance. 1372 | In addition, it catches any exceptions that might be thrown during the operation and encapsulates them in a failed result. 1373 | 1374 | ### Result.assertOk(result) 1375 | 1376 | Asserts that the provided result is successful. If the result is a failure, an error is thrown. 1377 | Useful in unit tests. 1378 | 1379 | #### Parameters 1380 | 1381 | - `result` the result instance to assert against. 1382 | 1383 | ### Result.assertError(result) 1384 | 1385 | Asserts that the provided result is a failure. If the result is successful, an error is thrown. 1386 | Useful in unit tests. 1387 | 1388 | #### Parameters 1389 | 1390 | - `result` the result instance to assert against. 1391 | 1392 | ## AsyncResult 1393 | 1394 | Represents the asynchronous outcome of an operation that can either succeed or fail. 1395 | 1396 | ```ts 1397 | class AsyncResult {} 1398 | ``` 1399 | 1400 | ### isAsyncResult 1401 | 1402 | Utility getter that checks if the current instance is an `AsyncResult`. 1403 | 1404 | ### toTuple() 1405 | 1406 | **returns** the result in a tuple format where the first element is the value and the second element is the error. 1407 | 1408 | If the result is successful, the error will be `null`. If the result is a failure, the value will be `null`. 1409 | This method is especially useful when you want to destructure the result into a tuple and use TypeScript's narrowing capabilities. 1410 | 1411 | #### Example 1412 | Narrowing down the type using destructuring 1413 | ```ts 1414 | declare const result: AsyncResult; 1415 | 1416 | const [value, error] = result.toTuple(); 1417 | 1418 | if (error) { 1419 | // error is ErrorA 1420 | } else { 1421 | // at this point the value must be a number 1422 | } 1423 | ``` 1424 | 1425 | ### errorOrNull() 1426 | 1427 | **returns** the encapsulated error if the result is a failure, otherwise `null`. 1428 | 1429 | ### getOrNull() 1430 | 1431 | **returns** the encapsulated value if the result is successful, otherwise `null`. 1432 | 1433 | ### getOrDefault(defaultValue) 1434 | 1435 | Retrieves the encapsulated value of the result, or a default value if the result is a failure. 1436 | 1437 | #### Parameters 1438 | 1439 | - `defaultValue` The value to return if the result is a failure. 1440 | 1441 | **returns** The encapsulated value if the result is successful, otherwise the default value. 1442 | 1443 | #### Example 1444 | obtaining the value of a result, or a default value 1445 | ```ts 1446 | declare const result: AsyncResult; 1447 | 1448 | const value = await result.getOrDefault(0); // number 1449 | ``` 1450 | 1451 | #### Example 1452 | using a different type for the default value 1453 | ```ts 1454 | declare const result: AsyncResult; 1455 | 1456 | const value = await result.getOrDefault("default"); // number | string 1457 | ``` 1458 | 1459 | ### getOrElse(onFailure) 1460 | 1461 | Retrieves the value of the result, or transforms the error using the `onFailure` callback into a value. 1462 | 1463 | #### Parameters 1464 | 1465 | - `onFailure` callback function which allows you to transform the error into a value. The callback can be async as well. 1466 | 1467 | **returns** either the value if the result is successful, or the transformed error. 1468 | 1469 | #### Example 1470 | transforming the error into a value 1471 | ```ts 1472 | declare const result: AsyncResult; 1473 | 1474 | const value = await result.getOrElse((error) => 0); // number 1475 | ``` 1476 | 1477 | #### Example 1478 | using an async callback 1479 | ```ts 1480 | const value = await result.getOrElse(async (error) => 0); // number 1481 | ``` 1482 | 1483 | ### getOrThrow() 1484 | 1485 | Retrieves the encapsulated value of the result, or throws an error if the result is a failure. 1486 | 1487 | **returns** The encapsulated value if the result is successful. 1488 | 1489 | **throws** the encapsulated error if the result is a failure. 1490 | 1491 | #### Example 1492 | obtaining the value of a result, or throwing an error 1493 | ```ts 1494 | declare const result: AsyncResult; 1495 | 1496 | const value = await result.getOrThrow(); // number 1497 | ``` 1498 | 1499 | ### fold(onSuccess, onFailure) 1500 | 1501 | Returns the result of the `onSuccess` callback when the result represents success or 1502 | the result of the `onFailure` callback when the result represents a failure. 1503 | 1504 | > [!NOTE] 1505 | > Any exceptions that might be thrown inside the callbacks are not caught, so it is your responsibility 1506 | > to handle these exceptions 1507 | 1508 | #### Parameters 1509 | 1510 | - `onSuccess` callback function to run when the result is successful. The callback can be async as well. 1511 | 1512 | - `onFailure` callback function to run when the result is a failure. The callback can be async as well. 1513 | 1514 | **returns** the result of the callback that was executed. 1515 | 1516 | #### Example 1517 | folding a result to a response-like object 1518 | 1519 | ```ts 1520 | declare const result: AsyncResult; 1521 | 1522 | const response = await result.fold( 1523 | (user) => ({ status: 200, body: user }), 1524 | (error) => { 1525 | switch (error.type) { 1526 | case "not-found": 1527 | return { status: 404, body: "User not found" }; 1528 | case "user-deactivated": 1529 | return { status: 403, body: "User is deactivated" }; 1530 | } 1531 | } 1532 | ); 1533 | ``` 1534 | 1535 | ### onFailure(action) 1536 | 1537 | Calls the `action` callback when the result represents a failure. It is meant to be used for 1538 | side-effects and the operation does not modify the result itself. 1539 | 1540 | #### Parameters 1541 | 1542 | - `action` callback function to run when the result is a failure. The callback can be async as well. 1543 | 1544 | **returns** the original instance of the result. 1545 | 1546 | > [!NOTE] 1547 | > Any exceptions that might be thrown inside the `action` callback are not caught, so it is your responsibility 1548 | > to handle these exceptions 1549 | 1550 | #### Example 1551 | adding logging between operations 1552 | ```ts 1553 | declare const result: AsyncResult; 1554 | 1555 | result 1556 | .onFailure((error) => console.error("I'm failing!", error)) 1557 | .map((value) => value 2); // proceed with other operations 1558 | ``` 1559 | 1560 | ### onSuccess(action) 1561 | 1562 | Calls the `action` callback when the result represents a success. It is meant to be used for 1563 | side-effects and the operation does not modify the result itself. 1564 | 1565 | #### Parameters 1566 | 1567 | - `action` callback function to run when the result is successful. The callback can be async as well. 1568 | 1569 | **returns** the original instance of the result. 1570 | 1571 | > [!NOTE] 1572 | > Any exceptions that might be thrown inside the `action` callback are not caught, so it is your responsibility 1573 | > to handle these exceptions 1574 | 1575 | #### Example 1576 | adding logging between operations 1577 | ```ts 1578 | declare const result: AsyncResult; 1579 | 1580 | result 1581 | .onSuccess((value) => console.log("I'm a success!", value)) 1582 | .map((value) => value 2); // proceed with other operations 1583 | ``` 1584 | 1585 | #### Example 1586 | using an async callback 1587 | ```ts 1588 | declare const result: AsyncResultResult; 1589 | 1590 | const asyncResult = await result.onSuccess(async (value) => someAsyncOperation(value)); 1591 | ``` 1592 | 1593 | ### map(transformFn) 1594 | 1595 | Transforms the value of a successful result using the `transform` callback. 1596 | The `transform` callback can also return other `Result` or [`AsyncResult`](#asyncresult) instances, 1597 | which will be returned as-is (the `Error` types will be merged). 1598 | The operation will be ignored if the result represents a failure. 1599 | 1600 | #### Parameters 1601 | 1602 | - `transformFn` callback function to transform the value of the result. The callback can be async as well. 1603 | 1604 | **returns** a new [`AsyncResult`](#asyncresult) instance with the transformed value 1605 | 1606 | > [!NOTE] 1607 | > Any exceptions that might be thrown inside the `transform` callback are not caught, so it is your responsibility 1608 | > to handle these exceptions. Please refer to [`AsyncResult.mapCatching`](#mapcatchingtransformfn-transformerrorfn-1) for a version that catches exceptions 1609 | > and encapsulates them in a failed result. 1610 | 1611 | #### Example 1612 | transforming the value of a result 1613 | ```ts 1614 | declare const result: AsyncResult; 1615 | 1616 | const transformed = result.map((value) => value 2); // AsyncResult 1617 | ``` 1618 | 1619 | #### Example 1620 | returning a result instance 1621 | ```ts 1622 | declare const result: AsyncResult; 1623 | declare function multiplyByTwo(value: number): Result; 1624 | 1625 | const transformed = result.map((value) => multiplyByTwo(value)); // AsyncResult 1626 | ``` 1627 | 1628 | #### Example 1629 | doing an async transformation 1630 | ```ts 1631 | declare const result: AsyncResult; 1632 | 1633 | const transformed = result.map(async (value) => value 2); // AsyncResult 1634 | ``` 1635 | 1636 | #### Example 1637 | returning an async result instance 1638 | 1639 | ```ts 1640 | declare const result: AsyncResult; 1641 | declare function storeValue(value: number): AsyncResult; 1642 | 1643 | const transformed = result.map((value) => storeValue(value)); // AsyncResult 1644 | ``` 1645 | 1646 | ### mapCatching(transformFn, transformErrorFn?) 1647 | 1648 | Like [`AsyncResult.map`](#maptransformfn-1) it transforms the value of a successful result using the `transformFn` callback. 1649 | In addition, it catches any exceptions that might be thrown inside the `transformFn` callback and encapsulates them 1650 | in a failed result. 1651 | 1652 | #### Parameters 1653 | 1654 | - `transformFn` callback function to transform the value of the result. The callback can be async as well. 1655 | - `transformErrorFn` optional callback function that transforms any caught error inside `transformFn` into a specific error. 1656 | 1657 | **returns** a new [`AsyncResult`](#asyncresult) instance with the transformed value 1658 | 1659 | ### mapError(transformFn) 1660 | 1661 | Transforms the error of a failed result using the `transform` callback into a new error. 1662 | This can be useful when you want to transform the error into a different error type, or when you want to provide more context to the error. 1663 | 1664 | #### Parameters 1665 | 1666 | - `transformFn` callback function to transform the error of the result. 1667 | 1668 | **returns** a new failed [`AsyncResult`](#asyncresult) instance with the transformed error. 1669 | 1670 | #### Example 1671 | 1672 | transforming the error into a different error type 1673 | 1674 | ```ts 1675 | const result = Result.try(() => fetch("https://example.com")) 1676 | .mapCatching((response) => response.json() as Promise) 1677 | .mapError((error) => new FetchDataError("Failed to fetch data", { cause: error })); 1678 | // AsyncResult; 1679 | ``` 1680 | 1681 | 1682 | ### recover(onFailure) 1683 | 1684 | Transforms a failed result using the `onFailure` callback into a successful result. Useful for falling back to 1685 | other scenarios when a previous operation fails. 1686 | The `onFailure` callback can also return other `Result` or [`AsyncResult`](#asyncresult) instances, 1687 | which will be returned as-is. 1688 | After a recovery, logically, the result can only be a success. Therefore, the error type is set to `never`, unless 1689 | the `onFailure` callback returns a result-instance with another error type. 1690 | 1691 | #### Parameters 1692 | 1693 | - `onFailure` callback function to transform the error of the result. The callback can be async as well. 1694 | 1695 | **returns** a new successful [`AsyncResult`](#asyncresult) instance when the result represents a failure, or the original instance 1696 | if it represents a success. 1697 | 1698 | > [!NOTE] 1699 | > Any exceptions that might be thrown inside the `onFailure` callback are not caught, so it is your responsibility 1700 | > to handle these exceptions. Please refer to [`AsyncResult.recoverCatching`](#recovercatchingonfailure-1) for a version that catches exceptions 1701 | > and encapsulates them in a failed result. 1702 | 1703 | #### Example 1704 | transforming the error into a value 1705 | Note: Since we recover after trying to persist in the database, we can assume that the `DbError` has been taken care 1706 | of and therefore it has been removed from the final result. 1707 | ```ts 1708 | declare function persistInDB(item: Item): AsyncResult; 1709 | declare function persistLocally(item: Item): AsyncResult; 1710 | 1711 | persistInDB(item).recover(() => persistLocally(item)); // AsyncResult 1712 | ``` 1713 | 1714 | ### recoverCatching(onFailure) 1715 | 1716 | Like [`AsyncResult.recover`](#recoveronfailure-1) it transforms a failed result using the `onFailure` callback into a successful result. 1717 | In addition, it catches any exceptions that might be thrown inside the `onFailure` callback and encapsulates them 1718 | in a failed result. 1719 | 1720 | #### Parameters 1721 | 1722 | - `onFailure` callback function to transform the error of the result. The callback can be async as well. 1723 | 1724 | **returns** a new successful [`AsyncResult`](#asyncresult) instance when the result represents a failure, or the original instance 1725 | if it represents a success. -------------------------------------------------------------------------------- /src/helpers.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, expectTypeOf, it } from "vitest"; 2 | import type { 3 | InferPromise, 4 | IsAsyncFunction, 5 | IsFunction, 6 | ListContains, 7 | ListContainsFunction, 8 | ListContainsPromiseOrAsyncFunction, 9 | Union, 10 | UnionContainsPromise, 11 | UnwrapList, 12 | } from "./helpers.js"; 13 | import { 14 | assertUnreachable, 15 | isAsyncFn, 16 | isFunction, 17 | isPromise, 18 | } from "./helpers.js"; 19 | 20 | describe("helpers", () => { 21 | describe("isPromise", () => { 22 | it("tells whether a value represents a promise", () => { 23 | expect(isPromise({})).toBe(false); 24 | expect(isPromise(null)).toBe(false); 25 | expect(isPromise(undefined)).toBe(false); 26 | expect(isPromise(Promise.resolve(12))).toBe(true); 27 | // biome-ignore lint/suspicious/noThenProperty: 28 | expect(isPromise({ then: () => {} })).toBe(true); 29 | }); 30 | }); 31 | 32 | describe("isFunction", () => { 33 | it("tells whether a value represents a function", () => { 34 | expect(isFunction(() => {})).toBe(true); 35 | expect(isFunction(async () => {})).toBe(true); 36 | expect(isFunction({})).toBe(false); 37 | }); 38 | }); 39 | 40 | describe("isAsyncFn", () => { 41 | it("tells whether a value represents an async function", () => { 42 | expect(isAsyncFn(() => {})).toBe(false); 43 | expect(isAsyncFn(() => Promise.resolve(12))).toBe(false); 44 | expect(isAsyncFn(async () => {})).toBe(true); 45 | }); 46 | }); 47 | 48 | describe("assertUnreachable", () => { 49 | it("throws an error when called", () => { 50 | // @ts-expect-error 51 | expect(() => assertUnreachable("")).toThrowError("Unreachable case: "); 52 | }); 53 | 54 | it("complains (TS) when not all cases are handles", () => { 55 | const value = "a" as "a" | "b"; 56 | switch (value) { 57 | case "a": 58 | break; 59 | default: 60 | // @ts-expect-error Argument of type is not assignable to parameter of type 'never' 61 | assertUnreachable(value); 62 | } 63 | }); 64 | }); 65 | 66 | describe("IsAsyncFunction", () => { 67 | it("tells whether a type represents an async function", () => { 68 | expectTypeOf number>>().toEqualTypeOf(); 69 | expectTypeOf< 70 | IsAsyncFunction<() => Promise> 71 | >().toEqualTypeOf(); 72 | }); 73 | }); 74 | 75 | describe("IsFunction", () => { 76 | it("tells whether a type represents a function", () => { 77 | expectTypeOf number>>().toEqualTypeOf(); 78 | expectTypeOf Promise>>().toEqualTypeOf(); 79 | expectTypeOf>().toEqualTypeOf(); 80 | }); 81 | }); 82 | 83 | describe("ListContains", () => { 84 | it("tells whether an entry in a list is true", () => { 85 | expectTypeOf>().toEqualTypeOf(); 86 | expectTypeOf>().toEqualTypeOf(); 87 | expectTypeOf>().toEqualTypeOf(); 88 | }); 89 | }); 90 | 91 | describe("ListContainsPromiseOrAsyncFunction", () => { 92 | it("tells whether a list contains a promise or async function", () => { 93 | expectTypeOf< 94 | ListContainsPromiseOrAsyncFunction<[() => void]> 95 | >().toEqualTypeOf(); 96 | expectTypeOf< 97 | ListContainsPromiseOrAsyncFunction<[() => Promise, () => void]> 98 | >().toEqualTypeOf(); 99 | expectTypeOf< 100 | ListContainsPromiseOrAsyncFunction<[() => Promise]> 101 | >().toEqualTypeOf(); 102 | }); 103 | }); 104 | 105 | describe("ListContainsFunction", () => { 106 | it("tells whether a list contains a function", () => { 107 | expectTypeOf void]>>().toEqualTypeOf(); 108 | expectTypeOf>().toEqualTypeOf(); 109 | expectTypeOf< 110 | ListContainsFunction<[1, () => void]> 111 | >().toEqualTypeOf(); 112 | }); 113 | }); 114 | 115 | describe("Union", () => { 116 | it("creates a union type from a list", () => { 117 | expectTypeOf>().toEqualTypeOf<"a" | "b" | "c">(); 118 | }); 119 | }); 120 | 121 | describe("UnwrapList", () => { 122 | it("unwraps a list of types", () => { 123 | expectTypeOf< 124 | UnwrapList<[{ a: "a" }, () => { b: "b" }, () => Promise<{ c: "c" }>]> 125 | >().toEqualTypeOf<[{ a: "a" }, { b: "b" }, { c: "c" }]>(); 126 | }); 127 | }); 128 | 129 | describe("InferPromise", () => { 130 | it("infers the type of a promise", () => { 131 | expectTypeOf>>().toEqualTypeOf<{ 132 | a: "a"; 133 | }>(); 134 | }); 135 | }); 136 | 137 | describe("UnionContainsPromise", () => { 138 | it("tells whether a union contains a promise", () => { 139 | expectTypeOf< 140 | UnionContainsPromise | number> 141 | >().toEqualTypeOf(); 142 | expectTypeOf< 143 | UnionContainsPromise 144 | >().toEqualTypeOf(); 145 | expectTypeOf< 146 | UnionContainsPromise> 147 | >().toEqualTypeOf(); 148 | }); 149 | }); 150 | }); 151 | -------------------------------------------------------------------------------- /src/helpers.ts: -------------------------------------------------------------------------------- 1 | export function isPromise(value: unknown): value is AnyPromise { 2 | /* c8 ignore next */ 3 | if (value === null || value === undefined) { 4 | return false; 5 | } 6 | 7 | if (typeof value !== "object") { 8 | return false; 9 | } 10 | 11 | return value instanceof Promise || "then" in value; 12 | } 13 | 14 | export function isFunction(value: unknown): value is AnyFunction { 15 | return typeof value === "function"; 16 | } 17 | 18 | export function isAsyncFn(fn: AnyFunction): fn is AnyAsyncFunction { 19 | return fn.constructor.name === "AsyncFunction"; 20 | } 21 | 22 | /** 23 | * Utility function to assert that a case is unreachable 24 | * @param value the value which to check for exhaustiveness 25 | * 26 | * @example 27 | * ```ts 28 | * declare const value: "a" | "b" | "c"; 29 | * 30 | * switch (value) { 31 | * case "a": 32 | * // do something 33 | * break; 34 | * case "b": 35 | * // do something 36 | * break; 37 | * default: assertUnreachable(value) // TS should complain here 38 | * } 39 | * 40 | * ``` 41 | */ 42 | export function assertUnreachable(value: never): never { 43 | throw new Error(`Unreachable case: ${value}`); 44 | } 45 | 46 | export type IsAsyncFunction = T extends AnyAsyncFunction ? true : false; 47 | 48 | type IsPromiseOrAsyncFunction = T extends AnyAsyncFunction 49 | ? true 50 | : T extends Promise 51 | ? true 52 | : false; 53 | 54 | export type IsFunction = T extends AnyFunction ? true : false; 55 | 56 | type IsPromise = T extends AnyPromise ? true : false; 57 | 58 | export type UnionContainsPromise = AnyPromise extends Union 59 | ? true 60 | : false; 61 | 62 | export type ListContains = Items[number] extends false 63 | ? false 64 | : true; 65 | 66 | export type ListContainsPromiseOrAsyncFunction = ListContains<{ 67 | [Index in keyof T]: IsPromiseOrAsyncFunction; 68 | }>; 69 | 70 | export type ListContainsFunction = ListContains<{ 71 | [Index in keyof T]: IsFunction; 72 | }>; 73 | 74 | export type ListContainsPromise = ListContains<{ 75 | [Index in keyof T]: IsPromise; 76 | }>; 77 | 78 | export type Union = T[number]; 79 | 80 | export type Unwrap = T extends (...args: any[]) => Promise 81 | ? U 82 | : T extends (...args: any[]) => infer U 83 | ? U 84 | : T extends Promise 85 | ? U 86 | : T; 87 | 88 | export type UnwrapList = { 89 | [Index in keyof Items]: Unwrap; 90 | }; 91 | 92 | export type InferPromise = T extends Promise ? U : never; 93 | 94 | export type AnyPromise = Promise; 95 | 96 | export type AnyFunction = (...args: any[]) => Returning; 97 | export type AnyAsyncFunction = ( 98 | ...args: any[] 99 | ) => Promise; 100 | 101 | export type NativeError = globalThis.Error; 102 | 103 | // biome-ignore lint/complexity/noBannedTypes: 104 | export type AnyValue = {}; 105 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /* v8 ignore next */ 2 | export { Result, AsyncResult } from "./result.js"; 3 | /* v8 ignore next */ 4 | export { assertUnreachable } from "./helpers.js"; 5 | -------------------------------------------------------------------------------- /src/integration.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import { assertUnreachable } from "./helpers.js"; 3 | import { Result } from "./result.js"; 4 | 5 | describe("User management app", () => { 6 | let count = 0; 7 | 8 | const isValidEmail = (email: string) => /\S+@\S+\.\S+/.test(email); 9 | const isValidName = (name: string) => name.length > 3 && name.length < 10; 10 | 11 | class ValidationError extends Error { 12 | readonly type = "validation-error"; 13 | } 14 | 15 | class NotFoundError extends Error { 16 | readonly type = "not-found-error"; 17 | } 18 | 19 | class EmailAlreadyExistsError extends Error { 20 | readonly type = "email-already-exists-error"; 21 | } 22 | 23 | class UserDto { 24 | constructor( 25 | readonly id: number, 26 | readonly name: string, 27 | readonly email: string, 28 | ) {} 29 | 30 | static fromUser(user: User) { 31 | return new UserDto(user.id, user.name, user.email); 32 | } 33 | } 34 | 35 | class User { 36 | private _name: string; 37 | private _email: string; 38 | 39 | private constructor( 40 | readonly id: number, 41 | name: string, 42 | email: string, 43 | ) { 44 | this._name = name; 45 | this._email = email; 46 | } 47 | 48 | get name() { 49 | return this._name; 50 | } 51 | 52 | get email() { 53 | return this._email; 54 | } 55 | 56 | updateEmail(email: string) { 57 | if (!isValidEmail(email)) { 58 | return Result.error(new ValidationError("invalid email")); 59 | } 60 | 61 | this._email = email; 62 | return Result.ok(this); 63 | } 64 | 65 | static create(name: string, email: string) { 66 | if (!isValidName(name)) { 67 | return Result.error(new ValidationError("invalid name")); 68 | } 69 | 70 | if (!isValidEmail(email)) { 71 | return Result.error(new ValidationError("invalid email")); 72 | } 73 | 74 | return Result.ok(new User(count++, name, email)); 75 | } 76 | } 77 | 78 | class UserRepository { 79 | private users: Record = {}; 80 | 81 | async save(user: User) { 82 | this.users[user.id] = user; 83 | } 84 | 85 | async findById(id: number) { 86 | const possibleUser = this.users[id]; 87 | if (!possibleUser) { 88 | return Result.error( 89 | new NotFoundError(`Cannot find user with id ${id}`), 90 | ); 91 | } 92 | 93 | return Result.ok(possibleUser); 94 | } 95 | 96 | async existsByEmail(email: string) { 97 | return Object.values(this.users).some((user) => user.email === email); 98 | } 99 | } 100 | 101 | class UserService { 102 | constructor(private userRepository: UserRepository) {} 103 | 104 | createUser(name: string, email: string) { 105 | return Result.fromAsync(this.userRepository.existsByEmail(email)) 106 | .map((exists) => exists && Result.error(new EmailAlreadyExistsError())) 107 | .map(() => User.create(name, email)) 108 | .onSuccess((user) => this.userRepository.save(user)) 109 | .map(UserDto.fromUser); 110 | } 111 | 112 | updateUserEmail(id: number, email: string) { 113 | return Result.fromAsync(this.userRepository.findById(id)) 114 | .map((user) => user.updateEmail(email)) 115 | .onSuccess((user) => this.userRepository.save(user)) 116 | .map(UserDto.fromUser); 117 | } 118 | } 119 | 120 | class UserController { 121 | constructor(private userService: UserService) {} 122 | 123 | createUser(name: string, email: string) { 124 | return this.userService.createUser(name, email).fold( 125 | (user) => ({ status: 200 as const, data: user }), 126 | (error) => { 127 | switch (error.type) { 128 | case "validation-error": 129 | return { status: 400, data: { message: error.message } }; 130 | case "email-already-exists-error": 131 | return { 132 | status: 400, 133 | data: { message: "account with same email already exists" }, 134 | }; 135 | default: 136 | return assertUnreachable(error); 137 | } 138 | }, 139 | ); 140 | } 141 | 142 | updateUserEmail(id: number, email: string) { 143 | return this.userService.updateUserEmail(id, email).fold( 144 | (user) => ({ status: 200, data: user }), 145 | (error) => { 146 | switch (error.type) { 147 | case "not-found-error": 148 | return { status: 404, data: { message: error.message } }; 149 | case "validation-error": 150 | return { status: 400, data: { message: error.message } }; 151 | default: 152 | return assertUnreachable(error); 153 | } 154 | }, 155 | ); 156 | } 157 | } 158 | 159 | const createApp = () => 160 | new UserController(new UserService(new UserRepository())); 161 | 162 | it("creates a new user when the correct data is provided", async () => { 163 | const app = createApp(); 164 | const outcome = await app.createUser("John", "info@john.com"); 165 | 166 | expect(outcome).toEqual({ 167 | status: 200, 168 | data: { id: 0, name: "John", email: "info@john.com" }, 169 | }); 170 | }); 171 | 172 | it("does not create a new user when the e-mailaddress does not match the correct pattern", async () => { 173 | const app = createApp(); 174 | const outcome = await app.createUser("John", "invalidemail.com"); 175 | 176 | expect(outcome).toEqual({ 177 | status: 400, 178 | data: { message: "invalid email" }, 179 | }); 180 | }); 181 | 182 | it("does not create a new user when the name is too short", async () => { 183 | const app = createApp(); 184 | const outcome = await app.createUser("Jo", "info@john.com"); 185 | 186 | expect(outcome).toEqual({ 187 | status: 400, 188 | data: { message: "invalid name" }, 189 | }); 190 | }); 191 | 192 | it("does not create a new user when the e-mailaddress is already in use", async () => { 193 | const app = createApp(); 194 | 195 | const firstUserOutcome = await app.createUser("John", "info@john.com"); 196 | expect(firstUserOutcome.status).toBe(200); 197 | 198 | const secondUserOutcome = await app.createUser("John", "info@john.com"); 199 | expect(secondUserOutcome).toEqual({ 200 | status: 400, 201 | data: { message: "account with same email already exists" }, 202 | }); 203 | }); 204 | 205 | it("updates the e-mailaddress of a user", async () => { 206 | const app = createApp(); 207 | const createOutcome = await app.createUser("John", "info@john.com"); 208 | expect(createOutcome.status).toBe(200); 209 | 210 | const id = (createOutcome.data as UserDto).id; 211 | 212 | const updateOutcome = await app.updateUserEmail(id, "new@john.com"); 213 | expect(updateOutcome).toEqual({ 214 | status: 200, 215 | data: { id, name: "John", email: "new@john.com" }, 216 | }); 217 | }); 218 | 219 | it("does not update the e-mailaddress of a user when the e-mailaddress does not match the correct pattern", async () => { 220 | const app = createApp(); 221 | const createOutcome = await app.createUser("John", "info@john.com"); 222 | expect(createOutcome.status).toBe(200); 223 | 224 | const id = (createOutcome.data as UserDto).id; 225 | 226 | const updateOutcome = await app.updateUserEmail(id, "invalid.com"); 227 | expect(updateOutcome).toEqual({ 228 | status: 400, 229 | data: { message: "invalid email" }, 230 | }); 231 | }); 232 | 233 | it("does not update the e-mailaddress of a user the user does not exist", async () => { 234 | const app = createApp(); 235 | const updateOutcome = await app.updateUserEmail(2, "info@john.com"); 236 | expect(updateOutcome).toEqual({ 237 | status: 404, 238 | data: { message: "Cannot find user with id 2" }, 239 | }); 240 | }); 241 | }); 242 | -------------------------------------------------------------------------------- /src/result.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | AnyAsyncFunction, 3 | AnyFunction, 4 | AnyPromise, 5 | AnyValue, 6 | InferPromise, 7 | ListContainsFunction, 8 | ListContainsPromise, 9 | ListContainsPromiseOrAsyncFunction, 10 | NativeError, 11 | Union, 12 | UnionContainsPromise, 13 | Unwrap, 14 | UnwrapList, 15 | } from "./helpers.js"; 16 | import { isAsyncFn, isFunction, isPromise } from "./helpers.js"; 17 | 18 | // TODO: also add transformError fn to regular map function?? 19 | 20 | type InferError = T extends Result ? Error : never; 21 | type InferValue = T extends Result ? Value : T; 22 | 23 | type InferErrors = { 24 | [Index in keyof Items]: InferError; 25 | }; 26 | type InferValues = { 27 | [Index in keyof Items]: InferValue; 28 | }; 29 | 30 | type AnyResult = Result; 31 | type AnyAsyncResult = AsyncResult; 32 | 33 | type ValueOr = [Err] extends [never] 34 | ? [Value] extends [never] 35 | ? Or 36 | : Value 37 | : Value | Or; 38 | 39 | type ErrorOr = [Value] extends [never] 40 | ? [Err] extends [never] 41 | ? Or 42 | : Err 43 | : Err | Or; 44 | 45 | type AccountForFunctionThrowing = 46 | ListContainsFunction extends true 47 | ? NativeError 48 | : ListContainsPromise extends true 49 | ? NativeError 50 | : never; 51 | 52 | /** 53 | * Represents the asynchronous outcome of an operation that can either succeed or fail. 54 | */ 55 | export class AsyncResult extends Promise> { 56 | /** 57 | * Utility getter to infer the value type of the result. 58 | * Note: this getter does not hold any value, it's only used for type inference. 59 | */ 60 | declare $inferValue: Value; 61 | 62 | /** 63 | * Utility getter to infer the error type of the result. 64 | * Note: this getter does not hold any value, it's only used for type inference. 65 | */ 66 | declare $inferError: Err; 67 | 68 | /** 69 | * Utility getter to check if the current instance is an `AsyncResult`. 70 | */ 71 | get isAsyncResult(): true { 72 | return true; 73 | } 74 | 75 | /** 76 | * @returns the result in a tuple format where the first element is the value and the second element is the error. 77 | * If the result is successful, the error will be `null`. If the result is a failure, the value will be `null`. 78 | * 79 | * This method is especially useful when you want to destructure the result into a tuple and use TypeScript's narrowing capabilities. 80 | * 81 | * @example Narrowing down the result type using destructuring 82 | * ```ts 83 | * declare const result: AsyncResult; 84 | * 85 | * const [value, error] = await result.toTuple(); 86 | * 87 | * if (error) { 88 | * // error is ErrorA 89 | * return; 90 | * } 91 | * 92 | * // value must be a number 93 | * ``` 94 | */ 95 | async toTuple(): Promise< 96 | [Err] extends [never] 97 | ? [value: Value, error: never] 98 | : [Value] extends [never] 99 | ? [value: never, error: Err] 100 | : [value: Value, error: null] | [value: null, error: Err] 101 | > { 102 | const result = await this; 103 | return result.toTuple(); 104 | } 105 | 106 | /** 107 | * @returns the encapsulated error if the result is a failure, otherwise `null`. 108 | */ 109 | async errorOrNull(): Promise> { 110 | const result = (await this) as Result; 111 | return result.errorOrNull(); 112 | } 113 | 114 | /** 115 | * @returns the encapsulated value if the result is successful, otherwise `null`. 116 | */ 117 | async getOrNull(): Promise> { 118 | const result = (await this) as Result; 119 | return result.getOrNull(); 120 | } 121 | 122 | /** 123 | * Retrieves the encapsulated value of the result, or a default value if the result is a failure. 124 | * 125 | * @param defaultValue The value to return if the result is a failure. 126 | * 127 | * @returns The encapsulated value if the result is successful, otherwise the default value. 128 | * 129 | * @example 130 | * obtaining the value of a result, or a default value 131 | * ```ts 132 | * declare const result: AsyncResult; 133 | * 134 | * const value = await result.getOrDefault(0); // number 135 | * ``` 136 | * 137 | * @example 138 | * using a different type for the default value 139 | * ```ts 140 | * declare const result: AsyncResult; 141 | * 142 | * const value = await result.getOrDefault("default"); // number | string 143 | * ``` 144 | */ 145 | async getOrDefault(defaultValue: Value | Else): Promise { 146 | const result = (await this) as Result; 147 | return result.getOrDefault(defaultValue); 148 | } 149 | 150 | /** 151 | * Retrieves the value of the result, or transforms the error using the {@link onFailure} callback into a value. 152 | * 153 | * @param onFailure callback function which allows you to transform the error into a value. The callback can be async as well. 154 | * @returns either the value if the result is successful, or the transformed error. 155 | * 156 | * @example 157 | * transforming the error into a value 158 | * ```ts 159 | * declare const result: AsyncResult; 160 | * 161 | * const value = await result.getOrElse((error) => 0); // number 162 | * ``` 163 | * 164 | * @example 165 | * using an async callback 166 | * ```ts 167 | * const value = await result.getOrElse(async (error) => 0); // number 168 | * ``` 169 | */ 170 | async getOrElse(onFailure: (error: Err) => Else) { 171 | const result = (await this) as Result; 172 | return result.getOrElse(onFailure) as Promise>; 173 | } 174 | 175 | /** 176 | * Retrieves the encapsulated value of the result, or throws an error if the result is a failure. 177 | * 178 | * @returns The encapsulated value if the result is successful. 179 | * 180 | * @throws the encapsulated error if the result is a failure. 181 | * 182 | * @example 183 | * obtaining the value of a result, or throwing an error 184 | * ```ts 185 | * declare const result: AsyncResult; 186 | * 187 | * const value = await result.getOrThrow(); // number 188 | * ``` 189 | */ 190 | async getOrThrow(): Promise { 191 | const result = (await this) as Result; 192 | return result.getOrThrow(); 193 | } 194 | 195 | /** 196 | * Returns the result of the {@link onSuccess} callback when the result represents success or 197 | * the result of the {@link onFailure} callback when the result represents a failure. 198 | * 199 | * > [!NOTE] 200 | * > Any exceptions that might be thrown inside the callbacks are not caught, so it is your responsibility 201 | * > to handle these exceptions 202 | * 203 | * @param onSuccess callback function to run when the result is successful. The callback can be async as well. 204 | * @param onFailure callback function to run when the result is a failure. The callback can be async as well. 205 | * @returns the result of the callback that was executed. 206 | * 207 | * @example 208 | * folding a result to a response-like object 209 | * 210 | * ```ts 211 | * declare const result: AsyncResult; 212 | * 213 | * const response = await result.fold( 214 | * (user) => ({ status: 200, body: user }), 215 | * (error) => { 216 | * switch (error.type) { 217 | * case "not-found": 218 | * return { status: 404, body: "User not found" }; 219 | * case "user-deactivated": 220 | * return { status: 403, body: "User is deactivated" }; 221 | * } 222 | * } 223 | * ); 224 | * ``` 225 | */ 226 | async fold( 227 | onSuccess: (value: Value) => SuccessResult, 228 | onFailure: (error: Err) => FailureResult, 229 | ) { 230 | const result = (await this) as Result; 231 | return result.fold(onSuccess, onFailure) as Promise< 232 | Unwrap | Unwrap 233 | >; 234 | } 235 | 236 | /** 237 | * Calls the {@link action} callback when the result represents a failure. It is meant to be used for 238 | * side-effects and the operation does not modify the result itself. 239 | * 240 | * @param action callback function to run when the result is a failure. The callback can be async as well. 241 | * @returns the original instance of the result. 242 | * 243 | * > [!NOTE] 244 | * > Any exceptions that might be thrown inside the {@link action} callback are not caught, so it is your responsibility 245 | * > to handle these exceptions 246 | * 247 | * @example 248 | * adding logging between operations 249 | * ```ts 250 | * declare const result: AsyncResult; 251 | * 252 | * result 253 | * .onFailure((error) => console.error("I'm failing!", error)) 254 | * .map((value) => value * 2); // proceed with other operations 255 | * ``` 256 | */ 257 | onFailure( 258 | action: (error: Err) => void | Promise, 259 | ): AsyncResult { 260 | return new AsyncResult((resolve, reject) => 261 | this.then(async (result) => { 262 | try { 263 | if (result.isError()) { 264 | await action(result.error as Err); 265 | } 266 | resolve(result); 267 | } catch (e) { 268 | reject(e); 269 | } 270 | }).catch(reject), 271 | ); 272 | } 273 | 274 | /** 275 | * Calls the {@link action} callback when the result represents a success. It is meant to be used for 276 | * side-effects and the operation does not modify the result itself. 277 | * 278 | * @param action callback function to run when the result is successful. The callback can be async as well. 279 | * @returns the original instance of the result. 280 | * 281 | * > [!NOTE] 282 | * > Any exceptions that might be thrown inside the {@link action} callback are not caught, so it is your responsibility 283 | * > to handle these exceptions 284 | * 285 | * @example 286 | * adding logging between operations 287 | * ```ts 288 | * declare const result: AsyncResult; 289 | * 290 | * result 291 | * .onSuccess((value) => console.log("I'm a success!", value)) 292 | * .map((value) => value * 2); // proceed with other operations 293 | * ``` 294 | * 295 | * @example 296 | * using an async callback 297 | * ```ts 298 | * declare const result: AsyncResultResult; 299 | * 300 | * const asyncResult = await result.onSuccess(async (value) => someAsyncOperation(value)); 301 | * ``` 302 | */ 303 | onSuccess( 304 | action: (value: Value) => void | Promise, 305 | ): AsyncResult { 306 | return new AsyncResult((resolve, reject) => 307 | this.then(async (result) => { 308 | try { 309 | if (result.isOk()) { 310 | await action(result.value as Value); 311 | } 312 | resolve(result); 313 | } catch (error) { 314 | reject(error); 315 | } 316 | }).catch(reject), 317 | ); 318 | } 319 | 320 | /** 321 | * Transforms the value of a successful result using the {@link transform} callback. 322 | * The {@link transform} callback can also return other {@link Result} or {@link AsyncResult} instances, 323 | * which will be returned as-is (the `Error` types will be merged). 324 | * The operation will be ignored if the result represents a failure. 325 | * 326 | * @param transform callback function to transform the value of the result. The callback can be async as well. 327 | * @returns a new {@linkcode AsyncResult} instance with the transformed value 328 | * 329 | * > [!NOTE] 330 | * > Any exceptions that might be thrown inside the {@link transform} callback are not caught, so it is your responsibility 331 | * > to handle these exceptions. Please refer to {@linkcode AsyncResult.mapCatching} for a version that catches exceptions 332 | * > and encapsulates them in a failed result. 333 | * 334 | * @example 335 | * transforming the value of a result 336 | * ```ts 337 | * declare const result: AsyncResult; 338 | * 339 | * const transformed = result.map((value) => value * 2); // AsyncResult 340 | * ``` 341 | * 342 | * @example 343 | * returning a result instance 344 | * ```ts 345 | * declare const result: AsyncResult; 346 | * declare function multiplyByTwo(value: number): Result; 347 | * 348 | * const transformed = result.map((value) => multiplyByTwo(value)); // AsyncResult 349 | * ``` 350 | * 351 | * @example 352 | * doing an async transformation 353 | * ```ts 354 | * declare const result: AsyncResult; 355 | * 356 | * const transformed = result.map(async (value) => value * 2); // AsyncResult 357 | * ``` 358 | * 359 | * @example 360 | * returning an async result instance 361 | * 362 | * ```ts 363 | * declare const result: AsyncResult; 364 | * declare function storeValue(value: number): AsyncResult; 365 | * 366 | * const transformed = result.map((value) => storeValue(value)); // AsyncResult 367 | * ``` 368 | */ 369 | map(transform: (value: Value) => ReturnType) { 370 | return new AsyncResult((resolve, reject) => 371 | this.then((result) => { 372 | if (result.isOk()) { 373 | try { 374 | const returnValue = transform((result as { value: Value }).value); 375 | if (isPromise(returnValue)) { 376 | returnValue 377 | .then((value) => 378 | resolve(Result.isResult(value) ? value : Result.ok(value)), 379 | ) 380 | .catch(reject); 381 | } else { 382 | resolve( 383 | Result.isResult(returnValue) 384 | ? returnValue 385 | : Result.ok(returnValue), 386 | ); 387 | } 388 | } catch (error) { 389 | reject(error); 390 | } 391 | } else { 392 | resolve(result); 393 | } 394 | }).catch(reject), 395 | ) as ReturnType extends Promise 396 | ? PromiseValue extends Result 397 | ? AsyncResult 398 | : AsyncResult 399 | : ReturnType extends Result 400 | ? AsyncResult 401 | : AsyncResult; 402 | } 403 | 404 | /** 405 | * Like {@linkcode AsyncResult.map} it transforms the value of a successful result using the {@link transformValue} callback. 406 | * In addition, it catches any exceptions that might be thrown inside the {@link transformValue} callback and encapsulates them 407 | * in a failed result. 408 | * 409 | * @param transformValue callback function to transform the value of the result. The callback can be async as well. 410 | * @param transformError callback function to transform any potential caught error while transforming the value. 411 | * @returns a new {@linkcode AsyncResult} instance with the transformed value 412 | */ 413 | mapCatching( 414 | transformValue: (value: Value) => ReturnType, 415 | transformError?: (error: unknown) => ErrorType, 416 | ) { 417 | return new AsyncResult((resolve, reject) => { 418 | this.map(transformValue) 419 | .then((result: AnyResult) => resolve(result)) 420 | .catch((error: unknown) => { 421 | try { 422 | resolve( 423 | Result.error(transformError ? transformError(error) : error), 424 | ); 425 | } catch (err) { 426 | reject(err); 427 | } 428 | }); 429 | }) as ReturnType extends Promise 430 | ? PromiseValue extends Result 431 | ? AsyncResult 432 | : AsyncResult 433 | : ReturnType extends Result 434 | ? AsyncResult 435 | : AsyncResult; 436 | } 437 | 438 | /** 439 | * Transforms the encapsulated error of a failed result using the {@link transform} callback into a new error. 440 | * This can be useful for instance to capture similar or related errors and treat them as a single higher-level error type 441 | * @param transform callback function to transform the error of the result. 442 | * @returns new {@linkcode AsyncResult} instance with the transformed error. 443 | * 444 | * @example 445 | * transforming the error of a result 446 | * ```ts 447 | * const result = Result.try(() => fetch("https://example.com")) 448 | * .mapCatching((response) => response.json() as Promise) 449 | * .mapError((error) => new FetchDataError("Failed to fetch data", { cause: error })); 450 | * // AsyncResult; 451 | * ``` 452 | */ 453 | mapError(transform: (error: Err) => NewError) { 454 | return new AsyncResult((resolve, reject) => 455 | this.then(async (result) => { 456 | try { 457 | resolve(result.mapError(transform)); 458 | } catch (error) { 459 | reject(error); 460 | } 461 | }).catch(reject), 462 | ); 463 | } 464 | 465 | /** 466 | * Transforms a failed result using the {@link onFailure} callback into a successful result. Useful for falling back to 467 | * other scenarios when a previous operation fails. 468 | * The {@link onFailure} callback can also return other {@link Result} or {@link AsyncResult} instances, 469 | * which will be returned as-is. 470 | * After a recovery, logically, the result can only be a success. Therefore, the error type is set to `never`, unless 471 | * the {@link onFailure} callback returns a result-instance with another error type. 472 | * 473 | * @param onFailure callback function to transform the error of the result. The callback can be async as well. 474 | * @returns a new successful {@linkcode AsyncResult} instance when the result represents a failure, or the original instance 475 | * if it represents a success. 476 | * 477 | * > [!NOTE] 478 | * > Any exceptions that might be thrown inside the {@link onFailure} callback are not caught, so it is your responsibility 479 | * > to handle these exceptions. Please refer to {@linkcode AsyncResult.recoverCatching} for a version that catches exceptions 480 | * > and encapsulates them in a failed result. 481 | * 482 | * @example 483 | * transforming the error into a value 484 | * Note: Since we recover after trying to persist in the database, we can assume that the `DbError` has been taken care 485 | * of and therefore it has been removed from the final result. 486 | * ```ts 487 | * declare function persistInDB(item: Item): AsyncResult; 488 | * declare function persistLocally(item: Item): AsyncResult; 489 | * 490 | * persistInDB(item).recover(() => persistLocally(item)); // AsyncResult 491 | * ``` 492 | */ 493 | recover(onFailure: (error: Err) => ReturnType) { 494 | return new AsyncResult((resolve, reject) => 495 | this.then(async (result) => { 496 | try { 497 | const outcome = await result.recover(onFailure); 498 | resolve(outcome); 499 | } catch (error) { 500 | reject(error); 501 | } 502 | }).catch(reject), 503 | ) as ReturnType extends Promise 504 | ? PromiseValue extends Result 505 | ? AsyncResult 506 | : AsyncResult 507 | : ReturnType extends Result 508 | ? AsyncResult 509 | : AsyncResult; 510 | } 511 | 512 | /** 513 | * Like {@linkcode AsyncResult.recover} it transforms a failed result using the {@link onFailure} callback into a successful result. 514 | * In addition, it catches any exceptions that might be thrown inside the {@link onFailure} callback and encapsulates them 515 | * in a failed result. 516 | * 517 | * @param onFailure callback function to transform the error of the result. The callback can be async as well. 518 | * @returns a new successful {@linkcode AsyncResult} instance when the result represents a failure, or the original instance 519 | * if it represents a success. 520 | */ 521 | recoverCatching(onFailure: (error: Err) => ReturnType) { 522 | return new AsyncResult((resolve, reject) => 523 | this.then((result) => { 524 | resolve(result.recoverCatching(onFailure)); 525 | }).catch(reject), 526 | ) as ReturnType extends Promise 527 | ? PromiseValue extends Result 528 | ? AsyncResult 529 | : AsyncResult 530 | : ReturnType extends Result 531 | ? AsyncResult 532 | : AsyncResult; 533 | } 534 | 535 | /** 536 | * Print-friendly representation of the `AsyncResult` instance. 537 | */ 538 | override toString(): string { 539 | return "AsyncResult"; 540 | } 541 | 542 | /** 543 | * @internal 544 | */ 545 | static error(error: Error): AsyncResult { 546 | return new AsyncResult((resolve) => resolve(Result.error(error))); 547 | } 548 | 549 | /** 550 | * @internal 551 | */ 552 | static ok(value: Value): AsyncResult { 553 | return new AsyncResult((resolve) => resolve(Result.ok(value))); 554 | } 555 | 556 | /** 557 | * @internal 558 | */ 559 | static fromPromise(promise: AnyPromise) { 560 | return new AsyncResult((resolve, reject) => { 561 | promise 562 | .then((value) => 563 | resolve(Result.isResult(value) ? value : Result.ok(value)), 564 | ) 565 | .catch(reject); 566 | }); 567 | } 568 | 569 | /** 570 | * @internal 571 | */ 572 | static fromPromiseCatching( 573 | promise: AnyPromise, 574 | transform?: (error: unknown) => unknown, 575 | ) { 576 | return new AsyncResult((resolve) => { 577 | promise 578 | .then((value) => 579 | resolve(Result.isResult(value) ? value : Result.ok(value)), 580 | ) 581 | .catch((caughtError) => { 582 | resolve(Result.error(transform?.(caughtError) ?? caughtError)); 583 | }); 584 | }); 585 | } 586 | } 587 | 588 | /** 589 | * Represents the outcome of an operation that can either succeed or fail. 590 | */ 591 | export class Result { 592 | private constructor( 593 | private readonly _value: Value, 594 | private readonly _error: Err, 595 | ) {} 596 | 597 | /** 598 | * Utility getter to infer the value type of the result. 599 | * Note: this getter does not hold any value, it's only used for type inference. 600 | */ 601 | declare $inferValue: Value; 602 | 603 | /** 604 | * Utility getter to infer the error type of the result. 605 | * Note: this getter does not hold any value, it's only used for type inference. 606 | */ 607 | declare $inferError: Err; 608 | 609 | /** 610 | * Utility getter that checks if the current instance is a `Result`. 611 | */ 612 | get isResult(): true { 613 | return true; 614 | } 615 | 616 | /** 617 | * Retrieves the encapsulated value of the result. 618 | * 619 | * @returns The value if the operation was successful, otherwise `undefined`. 620 | * 621 | * __Note:__ You can use {@linkcode Result.isOk} to narrow down the type to a successful result. 622 | * 623 | * @example 624 | * obtaining the value of a result, without checking if it's successful 625 | * ```ts 626 | * declare const result: Result; 627 | * 628 | * result.value; // number | undefined 629 | * ``` 630 | * 631 | * @example 632 | * obtaining the value of a result, after checking for success 633 | * ```ts 634 | * declare const result: Result; 635 | * 636 | * if (result.isOk()) { 637 | * result.value; // number 638 | * } 639 | * ``` 640 | */ 641 | get value(): ValueOr { 642 | return this._value as any; 643 | } 644 | 645 | /** 646 | * Retrieves the encapsulated error of the result. 647 | * 648 | * @returns The error if the operation failed, otherwise `undefined`. 649 | * 650 | * > [!NOTE] 651 | * > You can use {@linkcode Result.isError} to narrow down the type to a failed result. 652 | * 653 | * @example 654 | * obtaining the value of a result, without checking if it's a failure 655 | * ```ts 656 | * declare const result: Result; 657 | * 658 | * result.error; // Error | undefined 659 | * ``` 660 | * 661 | * @example 662 | * obtaining the error of a result, after checking for failure 663 | * ```ts 664 | * declare const result: Result; 665 | * 666 | * if (result.isError()) { 667 | * result.error; // Error 668 | * } 669 | * ``` 670 | */ 671 | get error(): ErrorOr { 672 | return this._error as any; 673 | } 674 | 675 | private get success() { 676 | return this.error === undefined; 677 | } 678 | 679 | private get failure() { 680 | return this.error !== undefined; 681 | } 682 | 683 | /** 684 | * Type guard that checks whether the result is successful. 685 | * 686 | * @returns `true` if the result is successful, otherwise `false`. 687 | * 688 | * @example 689 | * checking if a result is successful 690 | * ```ts 691 | * declare const result: Result; 692 | * 693 | * if (result.isOk()) { 694 | * result.value; // number 695 | * } 696 | * ``` 697 | */ 698 | isOk(): this is Result<[Value] extends [never] ? AnyValue : Value, never> { 699 | return this.success; 700 | } 701 | 702 | /** 703 | * Type guard that checks whether the result is successful. 704 | * 705 | * @returns `true` if the result represents a failure, otherwise `false`. 706 | * 707 | * @example 708 | * checking if a result represents a failure 709 | * ```ts 710 | * declare const result: Result; 711 | * 712 | * if (result.isError()) { 713 | * result.error; // Error 714 | * } 715 | * ``` 716 | */ 717 | isError(): this is Result { 718 | return this.failure; 719 | } 720 | 721 | /** 722 | * @returns the result in a tuple format where the first element is the value and the second element is the error. 723 | * If the result is successful, the error will be `null`. If the result is a failure, the value will be `null`. 724 | * 725 | * This method is especially useful when you want to destructure the result into a tuple and use TypeScript's narrowing capabilities. 726 | * 727 | * @example Narrowing down the result type using destructuring 728 | * ```ts 729 | * declare const result: Result; 730 | * 731 | * const [value, error] = result.toTuple(); 732 | * 733 | * if (error) { 734 | * // error is ErrorA 735 | * return; 736 | * } 737 | * 738 | * // value must be a number 739 | * ``` 740 | */ 741 | toTuple() { 742 | return [this._value ?? null, this._error ?? null] as [Err] extends [never] 743 | ? [value: Value, error: never] 744 | : [Value] extends [never] 745 | ? [value: never, error: Err] 746 | : [value: Value, error: null] | [value: null, error: Err]; 747 | } 748 | 749 | /** 750 | * @returns the encapsulated error if the result is a failure, otherwise `null`. 751 | */ 752 | errorOrNull() { 753 | return (this.failure ? this._error : null) as ErrorOr; 754 | } 755 | 756 | /** 757 | * @returns the encapsulated value if the result is successful, otherwise `null`. 758 | */ 759 | getOrNull() { 760 | return (this.success ? this._value : null) as ValueOr; 761 | } 762 | 763 | /** 764 | * Retrieves the value of the result, or a default value if the result is a failure. 765 | * 766 | * @param defaultValue The value to return if the result is a failure. 767 | * 768 | * @returns The encapsulated value if the result is successful, otherwise the default value. 769 | * 770 | * @example 771 | * obtaining the value of a result, or a default value 772 | * ```ts 773 | * declare const result: Result; 774 | * 775 | * const value = result.getOrDefault(0); // number 776 | * ``` 777 | * 778 | * @example 779 | * using a different type for the default value 780 | * ```ts 781 | * declare const result: Result; 782 | * 783 | * const value = result.getOrDefault("default"); // number | string 784 | * ``` 785 | */ 786 | getOrDefault(defaultValue: Else): Value | Else { 787 | return this.success ? this._value : defaultValue; 788 | } 789 | 790 | /** 791 | * Retrieves the value of the result, or transforms the error using the {@link onFailure} callback into a value. 792 | * 793 | * @param onFailure callback function which allows you to transform the error into a value. The callback can be async as well. 794 | * @returns either the value if the result is successful, or the transformed error. 795 | * 796 | * @example 797 | * transforming the error into a value 798 | * ```ts 799 | * declare const result: Result; 800 | * 801 | * const value = result.getOrElse((error) => 0); // number 802 | * ``` 803 | * 804 | * @example 805 | * using an async callback 806 | * ```ts 807 | * const value = await result.getOrElse(async (error) => 0); // Promise 808 | * ``` 809 | */ 810 | getOrElse( 811 | onFailure: (error: Err) => Else, 812 | ): Else extends Promise ? Promise : Value | Else { 813 | if (isAsyncFn(onFailure)) { 814 | return this.success 815 | ? Promise.resolve(this._value) 816 | : (onFailure(this._error) as any); 817 | } 818 | 819 | return this.success ? this._value : (onFailure(this._error) as any); 820 | } 821 | 822 | /** 823 | * Retrieves the value of the result, or throws an error if the result is a failure. 824 | * 825 | * @returns The value if the result is successful. 826 | * 827 | * @throws the encapsulated error if the result is a failure. 828 | * 829 | * @example 830 | * obtaining the value of a result, or throwing an error 831 | * ```ts 832 | * declare const result: Result; 833 | * 834 | * const value = result.getOrThrow(); // number 835 | * ``` 836 | */ 837 | getOrThrow(): Value { 838 | if (this.success) { 839 | return this._value; 840 | } 841 | 842 | throw this._error; 843 | } 844 | 845 | /** 846 | * Returns the result of the {@link onSuccess} callback when the result represents success or 847 | * the result of the {@link onFailure} callback when the result represents a failure. 848 | * 849 | * > [!NOTE] 850 | * > Any exceptions that might be thrown inside the callbacks are not caught, so it is your responsibility 851 | * > to handle these exceptions 852 | * 853 | * @param onSuccess callback function to run when the result is successful. The callback can be async as well. 854 | * @param onFailure callback function to run when the result is a failure. The callback can be async as well. 855 | * @returns the result of the callback that was executed. 856 | * 857 | * @example 858 | * folding a result to a response-like object 859 | * 860 | * ```ts 861 | * declare const result: Result; 862 | * 863 | * const response = result.fold( 864 | * (user) => ({ status: 200, body: user }), 865 | * (error) => { 866 | * switch (error.type) { 867 | * case "not-found": 868 | * return { status: 404, body: "User not found" }; 869 | * case "user-deactivated": 870 | * return { status: 403, body: "User is deactivated" }; 871 | * } 872 | * } 873 | * ); 874 | * ``` 875 | */ 876 | fold( 877 | onSuccess: (value: Value) => SuccessResult, 878 | onFailure: (error: Err) => FailureResult, 879 | ) { 880 | const isAsync = isAsyncFn(onSuccess) || isAsyncFn(onFailure); 881 | 882 | const outcome = this.success 883 | ? onSuccess(this._value) 884 | : onFailure(this._error); 885 | 886 | return ( 887 | isAsync && !isPromise(outcome) ? Promise.resolve(outcome) : outcome 888 | ) as UnionContainsPromise extends true 889 | ? Promise | Unwrap> 890 | : SuccessResult | FailureResult; 891 | } 892 | 893 | /** 894 | * Calls the {@link action} callback when the result represents a failure. It is meant to be used for 895 | * side-effects and the operation does not modify the result itself. 896 | * 897 | * @param action callback function to run when the result is a failure. The callback can be async as well. 898 | * @returns the original instance of the result. 899 | * 900 | * > [!NOTE] 901 | * > Any exceptions that might be thrown inside the {@link action} callback are not caught, so it is your responsibility 902 | * > to handle these exceptions 903 | * 904 | * @example 905 | * adding logging between operations 906 | * ```ts 907 | * declare const result: Result; 908 | * 909 | * result 910 | * .onFailure((error) => console.error("I'm failing!", error)) 911 | * .map((value) => value * 2); // proceed with other operations 912 | * ``` 913 | */ 914 | onFailure( 915 | action: (error: Err) => ReturnValue, 916 | ): ReturnValue extends AnyPromise ? AsyncResult : this { 917 | const isAsync = isAsyncFn(action); 918 | 919 | if (this.failure) { 920 | const outcome = action(this._error); 921 | if (isAsync) { 922 | return new AsyncResult((resolve) => { 923 | (outcome as AnyPromise).then(() => 924 | resolve(Result.error(this._error)), 925 | ); 926 | }) as any; 927 | } 928 | 929 | return this as any; 930 | } 931 | 932 | return isAsync ? AsyncResult.ok(this._value) : (this as any); 933 | } 934 | 935 | /** 936 | * Calls the {@link action} callback when the result represents a success. It is meant to be used for 937 | * side-effects and the operation does not modify the result itself. 938 | * 939 | * @param action callback function to run when the result is successful. The callback can be async as well. 940 | * @returns the original instance of the result. If the callback is async, it returns a new {@link AsyncResult} instance. 941 | * 942 | * > [!NOTE] 943 | * > Any exceptions that might be thrown inside the {@link action} callback are not caught, so it is your responsibility 944 | * > to handle these exceptions 945 | * 946 | * @example 947 | * adding logging between operations 948 | * ```ts 949 | * declare const result: Result; 950 | * 951 | * result 952 | * .onSuccess((value) => console.log("I'm a success!", value)) 953 | * .map((value) => value * 2); // proceed with other operations 954 | * ``` 955 | * 956 | * @example 957 | * using an async callback 958 | * ```ts 959 | * declare const result: Result; 960 | * 961 | * const asyncResult = await result.onSuccess(async (value) => someAsyncOperation(value)); 962 | * ``` 963 | */ 964 | onSuccess(action: (value: Value) => Promise): AsyncResult; 965 | onSuccess(action: (value: Value) => void): this; 966 | onSuccess(action: (value: Value) => unknown): unknown { 967 | const isAsync = isAsyncFn(action); 968 | 969 | if (this.success) { 970 | const outcome = action(this._value); 971 | if (isAsync) { 972 | return new AsyncResult((resolve) => { 973 | (outcome as AnyPromise).then(() => resolve(Result.ok(this._value))); 974 | }); 975 | } 976 | 977 | return this; 978 | } 979 | 980 | return isAsync ? AsyncResult.error(this._error) : this; 981 | } 982 | 983 | /** 984 | * Transforms the value of a successful result using the {@link transform} callback. 985 | * The {@link transform} callback can also return other {@link Result} or {@link AsyncResult} instances, 986 | * which will be returned as-is (the `Error` types will be merged). 987 | * The operation will be ignored if the result represents a failure. 988 | * 989 | * @param transform callback function to transform the value of the result. The callback can be async as well. 990 | * @returns a new {@linkcode Result} instance with the transformed value, or a new {@linkcode AsyncResult} instance 991 | * if the transform function is async. 992 | * 993 | * > [!NOTE] 994 | * > Any exceptions that might be thrown inside the {@link transform} callback are not caught, so it is your responsibility 995 | * > to handle these exceptions. Please refer to {@linkcode Result.mapCatching} for a version that catches exceptions 996 | * > and encapsulates them in a failed result. 997 | * 998 | * @example 999 | * transforming the value of a result 1000 | * ```ts 1001 | * declare const result: Result; 1002 | * 1003 | * const transformed = result.map((value) => value * 2); // Result 1004 | * ``` 1005 | * 1006 | * @example 1007 | * returning a result instance 1008 | * ```ts 1009 | * declare const result: Result; 1010 | * declare function multiplyByTwo(value: number): Result; 1011 | * 1012 | * const transformed = result.map((value) => multiplyByTwo(value)); // Result 1013 | * ``` 1014 | * 1015 | * @example 1016 | * doing an async transformation 1017 | * ```ts 1018 | * declare const result: Result; 1019 | * 1020 | * const transformed = result.map(async (value) => value * 2); // AsyncResult 1021 | * ``` 1022 | * 1023 | * @example 1024 | * returning an async result instance 1025 | * 1026 | * ```ts 1027 | * declare const result: Result; 1028 | * declare function storeValue(value: number): AsyncResult; 1029 | * 1030 | * const transformed = result.map((value) => storeValue(value)); // AsyncResult 1031 | * ``` 1032 | */ 1033 | map(transform: (value: Value) => ReturnType) { 1034 | return ( 1035 | this.success 1036 | ? Result.run(() => transform(this._value)) 1037 | : isAsyncFn(transform) 1038 | ? AsyncResult.error(this._error) 1039 | : this 1040 | ) as ReturnType extends Promise 1041 | ? PromiseValue extends Result 1042 | ? AsyncResult 1043 | : AsyncResult 1044 | : ReturnType extends Result 1045 | ? Result 1046 | : Result; 1047 | } 1048 | 1049 | /** 1050 | * Like {@linkcode Result.map} it transforms the value of a successful result using the {@link transformValue} callback. 1051 | * In addition, it catches any exceptions that might be thrown inside the {@link transformValue} callback and encapsulates them 1052 | * in a failed result. 1053 | * 1054 | * @param transformValue callback function to transform the value of the result. The callback can be async as well. 1055 | * @param transformError callback function to transform any potential caught error while transforming the value. 1056 | * @returns a new {@linkcode Result} instance with the transformed value, or a new {@linkcode AsyncResult} instance 1057 | * if the transform function is async. 1058 | */ 1059 | mapCatching( 1060 | transformValue: (value: Value) => ReturnType, 1061 | transformError?: (err: unknown) => ErrorType, 1062 | ) { 1063 | return ( 1064 | this.success 1065 | ? Result.try( 1066 | () => transformValue(this._value), 1067 | transformError as AnyFunction, 1068 | ) 1069 | : this 1070 | ) as ReturnType extends Promise 1071 | ? PromiseValue extends Result 1072 | ? AsyncResult 1073 | : AsyncResult 1074 | : ReturnType extends Result 1075 | ? Result 1076 | : Result; 1077 | } 1078 | 1079 | /** 1080 | * Transforms the encapsulated error of a failed result using the {@link transform} callback into a new error. 1081 | * This can be useful for instance to capture similar or related errors and treat them as a single higher-level error type 1082 | * @param transform callback function to transform the error of the result. 1083 | * @returns new {@linkcode Result} instance with the transformed error. 1084 | * 1085 | * @example 1086 | * transforming the error of a result 1087 | * ```ts 1088 | * declare const result: Result; 1089 | * 1090 | * result.mapError((error) => new ErrorB(error.message)); // Result 1091 | * ``` 1092 | */ 1093 | mapError( 1094 | transform: (error: Err) => NewError, 1095 | ): Result { 1096 | if (this.success) { 1097 | return this as Result; 1098 | } 1099 | 1100 | return Result.error(transform(this._error)); 1101 | } 1102 | 1103 | /** 1104 | * Transforms a failed result using the {@link onFailure} callback into a successful result. Useful for falling back to 1105 | * other scenarios when a previous operation fails. 1106 | * The {@link onFailure} callback can also return other {@link Result} or {@link AsyncResult} instances, 1107 | * which will be returned as-is. 1108 | * After a recovery, logically, the result can only be a success. Therefore, the error type is set to `never`, unless 1109 | * the {@link onFailure} callback returns a result-instance with another error type. 1110 | * 1111 | * @param onFailure callback function to transform the error of the result. The callback can be async as well. 1112 | * @returns a new successful {@linkcode Result} instance or a new successful {@linkcode AsyncResult} instance 1113 | * when the result represents a failure, or the original instance if it represents a success. 1114 | * 1115 | * > [!NOTE] 1116 | * > Any exceptions that might be thrown inside the {@link onFailure} callback are not caught, so it is your responsibility 1117 | * > to handle these exceptions. Please refer to {@linkcode Result.recoverCatching} for a version that catches exceptions 1118 | * > and encapsulates them in a failed result. 1119 | * 1120 | * @example 1121 | * transforming the error into a value 1122 | * Note: Since we recover after trying to persist in the database, we can assume that the `DbError` has been taken care 1123 | * of and therefore it has been removed from the final result. 1124 | * ```ts 1125 | * declare function persistInDB(item: Item): Result; 1126 | * declare function persistLocally(item: Item): Result; 1127 | * 1128 | * persistInDB(item).recover(() => persistLocally(item)); // Result 1129 | * ``` 1130 | */ 1131 | recover(onFailure: (error: Err) => ReturnType) { 1132 | return ( 1133 | this.success 1134 | ? isAsyncFn(onFailure) 1135 | ? AsyncResult.ok(this._value) 1136 | : this 1137 | : Result.run(() => onFailure(this._error)) 1138 | ) as ReturnType extends Promise 1139 | ? PromiseValue extends Result 1140 | ? AsyncResult 1141 | : AsyncResult 1142 | : ReturnType extends Result 1143 | ? Result 1144 | : Result; 1145 | } 1146 | 1147 | /** 1148 | * Like {@linkcode Result.recover} it transforms a failed result using the {@link onFailure} callback into a successful result. 1149 | * In addition, it catches any exceptions that might be thrown inside the {@link onFailure} callback and encapsulates them 1150 | * in a failed result. 1151 | * 1152 | * @param onFailure callback function to transform the error of the result. The callback can be async as well. 1153 | * @returns a new successful {@linkcode Result} instance or a new successful {@linkcode AsyncResult} instance 1154 | * when the result represents a failure, or the original instance if it represents a success. 1155 | */ 1156 | recoverCatching(onFailure: (error: Err) => ReturnType) { 1157 | return ( 1158 | this.success 1159 | ? isAsyncFn(onFailure) 1160 | ? AsyncResult.ok(this._value) 1161 | : this 1162 | : Result.try(() => onFailure(this._error)) 1163 | ) as ReturnType extends Promise 1164 | ? PromiseValue extends Result 1165 | ? AsyncResult 1166 | : AsyncResult 1167 | : ReturnType extends Result 1168 | ? Result 1169 | : Result; 1170 | } 1171 | 1172 | /** 1173 | * Returns a string representation of the result. 1174 | */ 1175 | toString(): string { 1176 | if (this.success) { 1177 | return `Result.ok(${this._value})`; 1178 | } 1179 | 1180 | return `Result.error(${this.error})`; 1181 | } 1182 | 1183 | /** 1184 | * Creates a new result instance that represents a successful outcome. 1185 | * 1186 | * @param value The value to encapsulate in the result. 1187 | * @returns a new {@linkcode Result} instance. 1188 | * 1189 | * @example 1190 | * ```ts 1191 | * const result = Result.ok(42); // Result 1192 | * ``` 1193 | */ 1194 | static ok(): Result; 1195 | static ok(value: Value): Result; 1196 | static ok(value?: unknown) { 1197 | return new Result(value, undefined); 1198 | } 1199 | 1200 | /** 1201 | * Creates a new result instance that represents a failed outcome. 1202 | * 1203 | * @param error The error to encapsulate in the result. 1204 | * @returns a new {@linkcode Result} instance. 1205 | * 1206 | * @example 1207 | * ```ts 1208 | * const result = Result.error(new NotFoundError()); // Result 1209 | * ``` 1210 | */ 1211 | static error(error: Error): Result { 1212 | return new Result(undefined as never, error); 1213 | } 1214 | 1215 | /** 1216 | * Type guard that checks whether the provided value is a {@linkcode Result} instance. 1217 | * 1218 | * @param possibleResult any value that might be a {@linkcode Result} instance. 1219 | * @returns true if the provided value is a {@linkcode Result} instance, otherwise false. 1220 | */ 1221 | static isResult(possibleResult: unknown): possibleResult is AnyResult { 1222 | return possibleResult instanceof Result; 1223 | } 1224 | 1225 | /** 1226 | * Type guard that checks whether the provided value is a {@linkcode AsyncResult} instance. 1227 | * 1228 | * @param possibleAsyncResult any value that might be a {@linkcode AsyncResult} instance. 1229 | * @returns true if the provided value is a {@linkcode AsyncResult} instance, otherwise false. 1230 | */ 1231 | static isAsyncResult( 1232 | possibleAsyncResult: unknown, 1233 | ): possibleAsyncResult is AnyAsyncResult { 1234 | return possibleAsyncResult instanceof AsyncResult; 1235 | } 1236 | 1237 | private static run(fn: AnyFunction): AnyResult | AnyAsyncResult { 1238 | const returnValue = fn(); 1239 | 1240 | if (isPromise(returnValue)) { 1241 | return AsyncResult.fromPromise(returnValue); 1242 | } 1243 | 1244 | return Result.isResult(returnValue) ? returnValue : Result.ok(returnValue); 1245 | } 1246 | 1247 | private static allInternal( 1248 | items: any[], 1249 | opts: { catching: boolean }, 1250 | ): AnyResult | AnyAsyncResult { 1251 | const runner = opts.catching ? Result.try : Result.run; 1252 | 1253 | const flattened: Array = []; 1254 | 1255 | let isAsync = items.some(isPromise); 1256 | let hasFailure = false; 1257 | 1258 | for (const item of items) { 1259 | if (isFunction(item)) { 1260 | if (hasFailure) { 1261 | continue; 1262 | } 1263 | 1264 | const returnValue = runner(item as AnyFunction); 1265 | 1266 | if (Result.isResult(returnValue) && returnValue.isError()) { 1267 | hasFailure = true; 1268 | if (!isAsync) { 1269 | return returnValue; 1270 | } 1271 | } 1272 | 1273 | if (Result.isAsyncResult(returnValue)) { 1274 | isAsync = true; 1275 | } 1276 | 1277 | flattened.push(returnValue); 1278 | } else if (Result.isResult(item)) { 1279 | if (item.isError()) { 1280 | hasFailure = true; 1281 | if (!isAsync) { 1282 | return item; 1283 | } 1284 | } 1285 | 1286 | flattened.push(item); 1287 | } else if (Result.isAsyncResult(item)) { 1288 | isAsync = true; 1289 | flattened.push(item); 1290 | } else if (isPromise(item)) { 1291 | isAsync = true; 1292 | 1293 | flattened.push( 1294 | opts.catching 1295 | ? AsyncResult.fromPromiseCatching(item) 1296 | : AsyncResult.fromPromise(item), 1297 | ); 1298 | } else { 1299 | flattened.push(Result.ok(item)); 1300 | } 1301 | } 1302 | 1303 | if (isAsync) { 1304 | return new AsyncResult((resolve, reject) => { 1305 | const asyncResults: AnyAsyncResult[] = []; 1306 | const asyncIndexes: number[] = []; 1307 | 1308 | for (let i = 0; i < flattened.length; i++) { 1309 | const item = flattened[i]; 1310 | if (Result.isAsyncResult(item)) { 1311 | asyncResults.push(item); 1312 | asyncIndexes.push(i); 1313 | } 1314 | } 1315 | 1316 | Promise.all(asyncResults) 1317 | .then((resolvedResults) => { 1318 | const merged = [...flattened] as AnyResult[]; 1319 | for (let i = 0; i < resolvedResults.length; i++) { 1320 | // biome-ignore lint/style/noNonNullAssertion: 1321 | merged[asyncIndexes[i]!] = resolvedResults[i]!; 1322 | } 1323 | 1324 | const firstFailedResult = merged.find((resolvedResult) => 1325 | resolvedResult.isError(), 1326 | ); 1327 | if (firstFailedResult) { 1328 | resolve(firstFailedResult); 1329 | return; 1330 | } 1331 | 1332 | resolve(Result.ok(merged.map((result) => result.getOrNull()))); 1333 | }) 1334 | .catch((reason) => { 1335 | // note: this should only happen when opts.catching is false 1336 | reject(reason); 1337 | }); 1338 | }); 1339 | } 1340 | 1341 | return Result.ok( 1342 | (flattened as AnyResult[]).map((result) => result.getOrNull()), 1343 | ); 1344 | } 1345 | 1346 | /** 1347 | * Similar to {@linkcode Promise.all}, but for results. 1348 | * Useful when you want to run multiple independent operations and bundle the outcome into a single result. 1349 | * All possible values of the individual operations are collected into an array. `Result.all` will fail eagerly, 1350 | * meaning that as soon as any of the operations fail, the entire result will be a failure. 1351 | * Each argument can be a mixture of literal values, functions, {@linkcode Result} or {@linkcode AsyncResult} instances, or {@linkcode Promise}. 1352 | * 1353 | * @param items one or multiple literal value, function, {@linkcode Result} or {@linkcode AsyncResult} instance, or {@linkcode Promise}. 1354 | * @returns combined result of all the operations. 1355 | * 1356 | * > [!NOTE] 1357 | * > Any exceptions that might be thrown are not caught, so it is your responsibility 1358 | * > to handle these exceptions. Please refer to {@linkcode Result.allCatching} for a version that catches exceptions 1359 | * > and encapsulates them in a failed result. 1360 | * 1361 | * @example 1362 | * basic usage 1363 | * ```ts 1364 | * declare function createTask(name: string): Result; 1365 | * 1366 | * const tasks = ["task-a", "task-b", "task-c"]; 1367 | * const result = Result.all(...tasks.map(createTask)); // Result 1368 | * ``` 1369 | * 1370 | * @example 1371 | * running multiple operations and combining the results 1372 | * ```ts 1373 | * const result = Result.all( 1374 | * "a", 1375 | * Promise.resolve("b"), 1376 | * Result.ok("c"), 1377 | * Result.try(async () => "d"), 1378 | * () => "e", 1379 | * () => Result.try(async () => "f"), 1380 | * () => Result.ok("g"), 1381 | * async () => "h", 1382 | * ); // AsyncResult<[string, string, string, string, string, string, string, string], Error> 1383 | * ``` 1384 | */ 1385 | static all>( 1386 | ...items: Items 1387 | ) { 1388 | return Result.allInternal(items, { 1389 | catching: false, 1390 | }) as ListContainsPromiseOrAsyncFunction extends true 1391 | ? AsyncResult, Union>> 1392 | : Result, Union>>; 1393 | } 1394 | 1395 | /** 1396 | * Similar to {@linkcode Result.all}, but catches any exceptions that might be thrown during the operations. 1397 | * @param items one or multiple literal value, function, {@linkcode Result} or {@linkcode AsyncResult} instance, or {@linkcode Promise}. 1398 | * @returns combined result of all the operations. 1399 | */ 1400 | static allCatching< 1401 | Items extends any[], 1402 | Unwrapped extends any[] = UnwrapList, 1403 | >(...items: Items) { 1404 | return Result.allInternal(items, { 1405 | catching: true, 1406 | }) as ListContainsPromiseOrAsyncFunction extends true 1407 | ? AsyncResult< 1408 | InferValues, 1409 | Union> | AccountForFunctionThrowing 1410 | > 1411 | : Result< 1412 | InferValues, 1413 | Union> | AccountForFunctionThrowing 1414 | >; 1415 | } 1416 | 1417 | /** 1418 | * Wraps a function and returns a new function that returns a result. Especially useful when you want to work with 1419 | * external functions that might throw exceptions. 1420 | * The returned function will catch any exceptions that might be thrown and encapsulate them in a failed result. 1421 | * 1422 | * @param fn function to wrap. Can be synchronous or asynchronous. 1423 | * @returns a new function that returns a result. 1424 | * 1425 | * @example 1426 | * basic usage 1427 | * ```ts 1428 | * declare function divide(a: number, b: number): number; 1429 | * 1430 | * const safeDivide = Result.wrap(divide); 1431 | * const result = safeDivide(10, 0); // Result 1432 | * ``` 1433 | */ 1434 | static wrap( 1435 | fn: Fn, 1436 | ): ( 1437 | ...args: Parameters 1438 | ) => AsyncResult>, NativeError>; 1439 | static wrap( 1440 | fn: Fn, 1441 | ): (...args: Parameters) => Result, NativeError>; 1442 | static wrap(fn: AnyFunction | AnyAsyncFunction): AnyFunction { 1443 | return function wrapped(...args: any[]) { 1444 | return Result.try(() => fn(...args)); 1445 | }; 1446 | } 1447 | 1448 | /** 1449 | * Executes the given {@linkcode fn} function and encapsulates the returned value as a successful result, or the 1450 | * thrown exception as a failed result. In a way, you can view this method as a try-catch block that returns a result. 1451 | * 1452 | * @param fn function with code to execute. Can be synchronous or asynchronous. 1453 | * @param transform optional callback to transform the caught error into a more meaningful error. 1454 | * @returns a new {@linkcode Result} instance. 1455 | * 1456 | * @example 1457 | * basic usage 1458 | * ```ts 1459 | * declare function saveFileToDisk(filename: string): void; // might throw an error 1460 | * 1461 | * const result = Result.try(() => saveFileToDisk("file.txt")); // Result 1462 | * ``` 1463 | * 1464 | * @example 1465 | * basic usage with error transformation 1466 | * ```ts 1467 | * declare function saveFileToDisk(filename: string): void; // might throw an error 1468 | * 1469 | * const result = Result.try( 1470 | * () => saveFileToDisk("file.txt"), 1471 | * (error) => new IOError("Failed to save file", { cause: error }) 1472 | * ); // Result 1473 | * ``` 1474 | */ 1475 | static try< 1476 | Fn extends AnyAsyncFunction, 1477 | R = InferPromise>, 1478 | >(fn: Fn): AsyncResult, InferError | NativeError>; 1479 | static try, R = ReturnType>( 1480 | fn: Fn, 1481 | ): Result, InferError | NativeError>; 1482 | static try( 1483 | fn: () => ReturnType, 1484 | ): AsyncResult, NativeError>; 1485 | static try(fn: () => ReturnType): Result; 1486 | static try( 1487 | fn: () => ReturnType, 1488 | transform: (error: unknown) => ErrorType, 1489 | ): AsyncResult, ErrorType>; 1490 | static try( 1491 | fn: () => ReturnType, 1492 | transform: (error: unknown) => ErrorType, 1493 | ): Result; 1494 | static try( 1495 | fn: AnyFunction | AnyAsyncFunction, 1496 | transform?: (error: unknown) => any, 1497 | ) { 1498 | try { 1499 | const returnValue = fn(); 1500 | 1501 | if (isPromise(returnValue)) { 1502 | return AsyncResult.fromPromiseCatching(returnValue, transform); 1503 | } 1504 | 1505 | return Result.isResult(returnValue) 1506 | ? returnValue 1507 | : Result.ok(returnValue); 1508 | } catch (caughtError: unknown) { 1509 | return Result.error(transform?.(caughtError) ?? caughtError); 1510 | } 1511 | } 1512 | 1513 | /** 1514 | * Utility method to transform a Promise, that holds a literal value or 1515 | * a {@linkcode Result} or {@linkcode AsyncResult} instance, into an {@linkcode AsyncResult} instance. Useful when you want to immediately chain operations 1516 | * after calling an async function. 1517 | * 1518 | * @param value a Promise that holds a literal value or a {@linkcode Result} or {@linkcode AsyncResult} instance. 1519 | * 1520 | * @returns a new {@linkcode AsyncResult} instance. 1521 | * 1522 | * > [!NOTE] 1523 | * > Any exceptions that might be thrown are not caught, so it is your responsibility 1524 | * > to handle these exceptions. Please refer to {@linkcode Result.fromAsyncCatching} for a version that catches exceptions 1525 | * > and encapsulates them in a failed result. 1526 | * 1527 | * @example 1528 | * basic usage 1529 | * 1530 | * ```ts 1531 | * declare function someAsyncOperation(): Promise>; 1532 | * 1533 | * // without 'Result.fromAsync' 1534 | * const result = (await someAsyncOperation()).map((value) => value * 2); // Result 1535 | * 1536 | * // with 'Result.fromAsync' 1537 | * const asyncResult = Result.fromAsync(someAsyncOperation()).map((value) => value * 2); // AsyncResult 1538 | * ``` 1539 | */ 1540 | static fromAsync>( 1541 | value: T, 1542 | ): T extends Promise> 1543 | ? AsyncResult 1544 | : never; 1545 | static fromAsync>( 1546 | value: T, 1547 | ): T extends Promise> ? AsyncResult : never; 1548 | static fromAsync( 1549 | value: T, 1550 | ): T extends Promise ? AsyncResult : never; 1551 | static fromAsync(value: unknown): unknown { 1552 | return Result.run(() => value); 1553 | } 1554 | 1555 | /** 1556 | * Similar to {@linkcode Result.fromAsync} this method transforms a Promise into an {@linkcode AsyncResult} instance. 1557 | * In addition, it catches any exceptions that might be thrown during the operation and encapsulates them in a failed result. 1558 | */ 1559 | static fromAsyncCatching>( 1560 | value: T, 1561 | ): T extends Promise> 1562 | ? AsyncResult 1563 | : never; 1564 | static fromAsyncCatching>( 1565 | value: T, 1566 | ): T extends Promise> 1567 | ? AsyncResult 1568 | : never; 1569 | static fromAsyncCatching( 1570 | value: T, 1571 | ): T extends Promise ? AsyncResult : never; 1572 | static fromAsyncCatching(value: unknown): unknown { 1573 | return Result.try(() => value); 1574 | } 1575 | 1576 | /** 1577 | * Asserts that the provided result is successful. If the result is a failure, an error is thrown. 1578 | * Useful in unit tests. 1579 | * 1580 | * @param result the result instance to assert against. 1581 | */ 1582 | static assertOk( 1583 | result: Result, 1584 | ): asserts result is Result { 1585 | if (result.isError()) { 1586 | throw new Error("Expected a successful result, but got an error instead"); 1587 | } 1588 | } 1589 | 1590 | /** 1591 | * Asserts that the provided result is a failure. If the result is successful, an error is thrown. 1592 | * Useful in unit tests. 1593 | * 1594 | * @param result the result instance to assert against. 1595 | */ 1596 | static assertError( 1597 | result: Result, 1598 | ): asserts result is Result { 1599 | if (result.isOk()) { 1600 | throw new Error("Expected a failed result, but got a value instead"); 1601 | } 1602 | } 1603 | } 1604 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "skipLibCheck": true, 5 | "target": "ESNext", 6 | "resolveJsonModule": true, 7 | "moduleDetection": "force", 8 | "isolatedModules": true, 9 | "strict": true, 10 | "noUncheckedIndexedAccess": true, 11 | "noImplicitOverride": true, 12 | "noImplicitReturns": true, 13 | "noUnusedLocals": true, 14 | "noUnusedParameters": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "moduleResolution": "NodeNext", 17 | "module": "NodeNext", 18 | "outDir": "dist", 19 | "sourceMap": true, 20 | "lib": ["ESNext"], 21 | "stripInternal": true 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "tsup"; 2 | 3 | export default defineConfig({ 4 | entry: ["src/index.ts"], 5 | splitting: false, 6 | sourcemap: true, 7 | clean: true, 8 | format: ["esm", "cjs"], 9 | target: "esnext", 10 | dts: true 11 | }); 12 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config"; 2 | 3 | export default defineConfig({ 4 | test: { 5 | testTimeout: 1000, 6 | coverage: { 7 | reporter: ['text'], 8 | thresholds: { 9 | "100": true 10 | } 11 | } 12 | } 13 | }) --------------------------------------------------------------------------------