Skip to content

Commit 4be9014

Browse files
committed
updates readme with info about generator support
1 parent 105b493 commit 4be9014

File tree

1 file changed

+209
-54
lines changed

1 file changed

+209
-54
lines changed

readme.md

Lines changed: 209 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
[![BUNDLEPHOBIA](https://badgen.net/bundlephobia/minzip/typescript-result)](https://bundlephobia.com/result?p=typescript-result)
88
[![Weekly downloads](https://badgen.net/npm/dw/typescript-result)](https://badgen.net/npm/dw/typescript-result)
99

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.
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.
1111

1212
## Table of contents
1313

@@ -59,8 +59,9 @@ Tested with Node.js version `16` and higher.
5959
Reading a JSON config file and validating its contents:
6060

6161
```typescript
62-
import { Result } from "typescript-result";
6362
import fs from "node:fs/promises";
63+
import { Result } from "typescript-result";
64+
import { s } from "some-schema-validation-library";
6465

6566
class IOError extends Error {
6667
readonly type = "io-error";
@@ -74,64 +75,67 @@ class ValidationError extends Error {
7475
readonly type = "validation-error";
7576
}
7677

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<string, unknown> =>
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-
}
78+
const readFile = Result.wrap(
79+
(filePath: string) => fs.readFile(filePath, "utf-8"),
80+
(error) => new IOError(`Unable to read file`, { cause: error }),
81+
);
10482

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}`,
83+
const parseConfig = Result.wrap(
84+
(data: unknown) =>
85+
s
86+
.object({
87+
name: s.string().min(1),
88+
version: s.number().int().positive(),
89+
})
90+
.parse(data),
91+
(error) => new ValidationError(`Invalid configuration`, { cause: error }),
92+
);
11493

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-
);
94+
// chaining style:
95+
const result = await readFile("config.json")
96+
.mapCatching(
97+
(contents) => JSON.parse(contents),
98+
(error) => new ParseError("Unable to parse JSON", { cause: error }),
99+
)
100+
.map((json) => parseConfig(json));
101+
102+
// generator style:
103+
const result = await Result.gen(function* () {
104+
const contents = yield* readFile("config.json");
105+
106+
const json = yield* Result.try(
107+
() => JSON.parse(contents),
108+
(error) => new ParseError("Unable to parse JSON", { cause: error }),
109+
);
110+
111+
return parseConfig(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+
return error.message;
125+
}
126+
},
127+
);
126128
```
127129

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).
129131

130132
## Why should you use a result type?
131133

132134
### Errors as values
133135

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.
135139

136140
### Ergonomic error handling
137141

@@ -169,7 +173,7 @@ if (firstAsyncResult.isOk()) {
169173
You can write:
170174

171175
```typescript
172-
const result = await Result.fromAsync(someAsyncFunction1())
176+
const result = await someAsyncFunction1()
173177
.map((value) => someAsyncFunction2(value))
174178
.map((value) => someAsyncFunction3(value))
175179
.fold(
@@ -250,10 +254,16 @@ function handleOrder(products: Product[], userId: number) {
250254
}
251255
```
252256

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+
253261
## Guide
254262

255263
### A note on errors
256264

265+
> Tldr: Tag your custom errors with a `readonly type` property to avoid TypeScript unifying them into a single `Error` type.
266+
257267
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.
258268

259269
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.
@@ -367,9 +377,30 @@ const safeWriteFile = Result.wrap(fs.writeFileSync);
367377
const result = safeWriteFile("file.txt", "Hello, World!", "utf-8"); // Result<void, Error>
368378
```
369379

380+
#### Using `Result.gen` and `Result.genCatching`
381+
382+
This library also supports the use of generator functions to create and work with results. For more info, see [using generators](#using-generators).
383+
To give you a feeling of how this works, here's a simple example:
384+
385+
```ts
386+
const result = Result.gen(function* () {
387+
const contents = yield* readFile("file.txt");
388+
const json = yield* parseJSON(contents);
389+
return parseConfig(json);
390+
}); // Result<Config, IOError | ParseError | ValidationError>
391+
```
392+
370393
### Performing operations on a result
371394

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.
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.
373404

374405
#### Chaining operations
375406

@@ -408,7 +439,131 @@ if (result.isOk()) {
408439

409440
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.
410441

411-
#### Transform: `map`, `mapCatching`, `recover`, `recoverCatching`, `mapError`
442+
#### Using generators
443+
444+
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.
453+
454+
**Chaining vs Generator Style**
455+
456+
Chaining style
457+
```ts
458+
declare function fetchTransactionAmount(transactionId: string):
459+
AsyncResult<number, UnableToFetchTransactionAmountError>;
460+
461+
declare function fetchDiscountRate(transactionId: string):
462+
AsyncResult<number, UnableToFetchDiscountRateError>;
463+
464+
function applyDiscount(total: number, discountRate: number) {
465+
if (discountRate === 0) {
466+
return Result.error(new InvalidDiscountRateError("Discount rate cannot be zero"));
467+
}
468+
469+
return Result.ok(total * (1 - discountRate));
470+
}
471+
472+
function getDiscountedPrice(transactionId: string) {
473+
return fetchTransactionAmount(transactionId)
474+
.map((amount) =>
475+
fetchDiscountRate()
476+
.recover(() => 0.1) // Default discount rate if fetching fails
477+
.map((discountRate) => applyDiscount(amount, discountRate)),
478+
)
479+
.map((finalAmount) => `Final amount to charge: ${finalAmount}`);
480+
}
481+
```
482+
483+
Generator style
484+
```ts
485+
declare function fetchTransactionAmount(transactionId: string):
486+
AsyncResult<number, UnableToFetchTransactionAmountError>;
487+
488+
declare function fetchDiscountRate(transactionId: string):
489+
AsyncResult<number, UnableToFetchDiscountRateError>;
490+
491+
function* applyDiscount(total: number, discountRate: number) {
492+
if (discountRate === 0) {
493+
return yield* Result.error(new InvalidDiscountRateError("Discount rate cannot be zero"));
494+
}
495+
496+
return total * (1 - discountRate);
497+
}
498+
499+
function* getDiscountedPrice(transactionId: string) {
500+
const amount = yield* fetchTransactionAmount(transactionId);
501+
502+
const discountRate = yield* fetchDiscountRate(transactionId)
503+
.recover(() => 0.1); // Default discount rate if fetching fails
504+
505+
const finalAmount = yield* applyDiscount(amount, discountRate);
506+
507+
return `Final amount to charge: ${finalAmount}`;
508+
}
509+
510+
// Usage
511+
const result = Result.gen(getPrice("transaction-123"));
512+
// AsyncResult<
513+
// string,
514+
// UnableToFetchTransactionAmountError | UnableToFetchDiscountRateError | InvalidDiscountRateError
515+
// >
516+
```
517+
518+
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(async function* () {
526+
const valueA = yield* someFn();
527+
528+
const valueB = await someAsyncFn(valueA);
529+
530+
return valueB;
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+
return tripled;
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:
551+
552+
```ts
553+
class OrderProcessor {
554+
private tax = 0.08;
555+
556+
processOrder(orderId: string) {
557+
return Result.gen(this, function* () {
558+
const order = yield* this.fetchOrder(orderId);
559+
const subtotal = yield* this.calculateSubtotal(order);
560+
return subtotal * (1 + this.tax);
561+
});
562+
}
563+
}
564+
```
565+
566+
#### Transform values and errors: `map`, `mapCatching`, `recover`, `recoverCatching`, `mapError`
412567

413568
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.
414569

0 commit comments

Comments
 (0)