You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.
10
+
Supercharge your TypeScript error handling with a powerful Result type that transforms chaotic try-catch blocks into elegant, type-safe code—catching bugs at compile time while making async operations seamless and your code much harder to break.
11
11
12
12
## Table of contents
13
13
@@ -59,8 +59,9 @@ Tested with Node.js version `16` and higher.
59
59
Reading a JSON config file and validating its contents:
60
60
61
61
```typescript
62
-
import { Result } from"typescript-result";
63
62
importfsfrom"node:fs/promises";
63
+
import { Result } from"typescript-result";
64
+
import { s } from"some-schema-validation-library";
64
65
65
66
classIOErrorextendsError {
66
67
readonly type ="io-error";
@@ -74,64 +75,67 @@ class ValidationError extends Error {
74
75
readonly type ="validation-error";
75
76
}
76
77
77
-
function readFile(path:string) {
78
-
returnResult.try(
79
-
() =>fs.readFile(path, "utf-8"),
80
-
(error) =>newIOError(`Unable to read file '${path}'`, { cause: error })
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
-
returnerror.message;
123
-
}
124
-
}
125
-
);
94
+
// chaining style:
95
+
const result =awaitreadFile("config.json")
96
+
.mapCatching(
97
+
(contents) =>JSON.parse(contents),
98
+
(error) =>newParseError("Unable to parse JSON", { cause: error }),
99
+
)
100
+
.map((json) =>parseConfig(json));
101
+
102
+
// generator style:
103
+
const result =awaitResult.gen(function* () {
104
+
const contents =yield*readFile("config.json");
105
+
106
+
const json =yield*Result.try(
107
+
() =>JSON.parse(contents),
108
+
(error) =>newParseError("Unable to parse JSON", { cause: error }),
109
+
);
110
+
111
+
returnparseConfig(json);
112
+
});
113
+
114
+
const message =result.fold(
115
+
(config) =>
116
+
`Successfully read config: name => ${config.name}, version => ${config.version}`,
117
+
(error) => {
118
+
switch (error.type) {
119
+
case"io-error":
120
+
return"Please check if the config file exists and is readable";
121
+
case"parse-error":
122
+
return"Please check if the config file contains valid JSON";
123
+
case"validation-error":
124
+
returnerror.message;
125
+
}
126
+
},
127
+
);
126
128
```
127
129
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.
130
+
For more examples, please check out the [examples directory](./examples).
129
131
130
132
## Why should you use a result type?
131
133
132
134
### Errors as values
133
135
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.
136
+
Instead of throwing exceptions where all errors are treated equally and disrupt your program's flow, the Result type embraces the 'errors-as-values' approach from functional programming. This lets you distinguish between expected errors (like "user not found") and unexpected ones (like system crashes), making expected errors part of your normal program flow.
137
+
138
+
By explicitly marking code that can succeed or fail with the Result type, TypeScript's type system tracks every possible failure scenario and forces you to handle them—catching bugs at compile time through exhaustive checking. This makes your code more reliable, maintainable, and transparent about what can go wrong.
135
139
136
140
### Ergonomic error handling
137
141
@@ -169,7 +173,7 @@ if (firstAsyncResult.isOk()) {
169
173
You can write:
170
174
171
175
```typescript
172
-
const result =awaitResult.fromAsync(someAsyncFunction1())
176
+
const result =awaitsomeAsyncFunction1()
173
177
.map((value) =>someAsyncFunction2(value))
174
178
.map((value) =>someAsyncFunction3(value))
175
179
.fold(
@@ -250,10 +254,16 @@ function handleOrder(products: Product[], userId: number) {
250
254
}
251
255
```
252
256
257
+
### Support for generators
258
+
259
+
Popularized by [EffectTS](https://effect.website/docs/getting-started/using-generators/), this library supports the use of generator functions to create and work with results. This allows you to write more imperative code that is easier to read and understand, while still benefiting from the type safety and error handling provided by the Result type. You don't have to use the generator syntax - it's fully optional. For more info, see [using generators](#using-generators).
260
+
253
261
## Guide
254
262
255
263
### A note on errors
256
264
265
+
> Tldr: Tag your custom errors with a `readonly type` property to avoid TypeScript unifying them into a single `Error` type.
266
+
257
267
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
268
259
269
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.
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.
395
+
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 and tools that lets you interact with the instance of a result in various ways. We will cover them in depth in the following sections below.
396
+
397
+
There are two styles of working with results:
398
+
- the more **functional** approach, also known as the [_chaining style_](#chaining-operations)
399
+
- the more **imperative** approach, also known as the [_generator style_](#using-generators)
400
+
401
+
In a way, the chaining style is similar to how you would _chain_ promises using `.then()`, while the more imperative style is similar to how you would use `async`/`await`. Both styles are equally valid and can be used interchangeably. The choice is mostly a matter of personal preference, but we will try to explain the benefits of each style.
402
+
403
+
Generally speaking: if you find yourself writing a lot of nested chains (e.g. `map`'s) or you often use loops or conditional logic, you are probably better off using the generator style. On the other hand, if you find yourself writing a lot of simple transformations that can be expressed in a single line, or you simple like the functional style, you are probably better off using the chaining style.
373
404
374
405
#### Chaining operations
375
406
@@ -408,7 +439,131 @@ if (result.isOk()) {
408
439
409
440
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.
Generator functions might look unfamiliar at first, but they offer a powerful way to write error-handling code that feels natural and imperative while maintaining all the type safety benefits of Results. The key insight is that with generators, you can write code that looks like normal sequential operations while automatically collecting all possible errors in the background.
445
+
446
+
**The golden rule**: Use `yield*` for every `Result` or `AsyncResult` operation. This gives you direct access to the success value without manual unwrapping.
447
+
448
+
Generators shine when you have:
449
+
-**Complex control flow** with conditionals and loops
450
+
-**Nested transformations** that become hard to read with chaining
451
+
452
+
Let's look at an example by comparing the chaining style with the generator style.
As you can see, the generator style reads more linear and is therefore easier to follow. This example also shows how you can not only yield (functions that return) `Result` or `AsyncResult`, but also nest logic in other generator functions as well.
519
+
520
+
**Async generators functions**
521
+
522
+
If you want to perform asynchronous operations inside a generator function, you can use a `async function*` callback:
523
+
524
+
```ts
525
+
const result =Result.gen(asyncfunction* () {
526
+
const valueA =yield*someFn();
527
+
528
+
const valueB =awaitsomeAsyncFn(valueA);
529
+
530
+
returnvalueB;
531
+
}); // AsyncResult
532
+
```
533
+
534
+
**Mixing styles**
535
+
536
+
You can combine generators with method chaining when it makes sense:
537
+
538
+
```ts
539
+
const result =Result.ok(12)
540
+
.map(function* (value) {
541
+
const doubled =yield*someOperation(value);
542
+
const tripled =yield*anotherOperation(doubled);
543
+
returntripled;
544
+
})
545
+
.map(finalValue=>`Result: ${finalValue}`);
546
+
```
547
+
548
+
**'This' context**
549
+
550
+
If you need access to the `this` context inside a generator function, you can use the overload of `Result.gen` or `Result.genCatching` by providing `this` is the first argument:
#### Transform values and errors: `map`, `mapCatching`, `recover`, `recoverCatching`, `mapError`
412
567
413
568
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.
0 commit comments