├── .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 |
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 |
2 |
3 | # TypeScript Result
4 |
5 | [](https://www.npmjs.com/package/typescript-result)
6 | [](http://www.typescriptlang.org/)
7 | [](https://bundlephobia.com/result?p=typescript-result)
8 | [](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