ripthrow 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +64 -0
- package/package.json +47 -0
- package/src/consumers/index.ts +3 -0
- package/src/consumers/match.ts +34 -0
- package/src/consumers/unwrap-or.ts +24 -0
- package/src/consumers/unwrap.ts +23 -0
- package/src/factories/bail.ts +17 -0
- package/src/factories/err.ts +19 -0
- package/src/factories/index.ts +7 -0
- package/src/factories/is-err.ts +19 -0
- package/src/factories/is-ok.ts +19 -0
- package/src/factories/ok.ts +19 -0
- package/src/factories/safe-async.ts +28 -0
- package/src/factories/safe.ts +26 -0
- package/src/index.ts +19 -0
- package/src/operators/and-then.ts +32 -0
- package/src/operators/context.ts +25 -0
- package/src/operators/index.ts +7 -0
- package/src/operators/map-err.ts +28 -0
- package/src/operators/map.ts +28 -0
- package/src/operators/or-else.ts +30 -0
- package/src/operators/tap-err.ts +23 -0
- package/src/operators/tap.ts +23 -0
- package/src/pattern.ts +157 -0
- package/src/report.ts +62 -0
- package/src/result-builder-async.ts +141 -0
- package/src/result-builder.ts +193 -0
- package/src/types/index.ts +1 -0
- package/src/types/result.ts +15 -0
- package/src/utils/all.ts +30 -0
- package/src/utils/any.ts +32 -0
- package/src/utils/index.ts +2 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sir Cesarium
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# ripthrow
|
|
2
|
+
|
|
3
|
+
**Zero-dependency, type-safe error handling for TypeScript.**
|
|
4
|
+
|
|
5
|
+
`ripthrow` is a lightweight library inspired by Rust's `Result` type and the proposed ECMAScript `?=` operator. It allows you to handle success and failure in a structured way, avoiding `try/catch` blocks and making error states explicit in your types.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **Type-Safe:** Full TypeScript support with robust type inference.
|
|
10
|
+
- **Functional API:** Chain operations with `map`, `andThen`, and `orElse`.
|
|
11
|
+
- **Safe Wrappers:** Convert throwing functions and Promises into `Result` types easily.
|
|
12
|
+
- **No Dependencies:** Extremely small footprint.
|
|
13
|
+
- **Documentation:** Built-in JSDoc with examples and a complete Wiki.
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
bun add github:MechanicalLabs/ripthrow
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Quick Start
|
|
22
|
+
|
|
23
|
+
### Explicit errors with `Ok` and `Err`
|
|
24
|
+
|
|
25
|
+
Functions return `Result`, making failure paths visible in the type signature:
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
import { Ok, Err, match, type Result } from "ripthrow";
|
|
29
|
+
|
|
30
|
+
function getUser(id: string): Result<{ id: string; name: string }, string> {
|
|
31
|
+
if (!id) {
|
|
32
|
+
return Err("ID is required");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return Ok({ id, name: "Alice" });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const result = getUser("123");
|
|
39
|
+
|
|
40
|
+
match(result, {
|
|
41
|
+
ok: (user) => console.log(user.name),
|
|
42
|
+
err: (msg) => console.error(`getUser function failed: ${msg}`),
|
|
43
|
+
});
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Wrap throwing code with `safe`
|
|
47
|
+
|
|
48
|
+
`safe` catches exceptions and returns a `Result` — no `try/catch`:
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
import { safe, build } from "ripthrow";
|
|
52
|
+
|
|
53
|
+
const raw = '{"valid": true}';
|
|
54
|
+
|
|
55
|
+
const isValid = build(safe(() => JSON.parse(raw)))
|
|
56
|
+
.map((data: any) => data.valid)
|
|
57
|
+
.unwrapOr(false);
|
|
58
|
+
|
|
59
|
+
console.log(isValid); // true
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Why ripthrow?
|
|
63
|
+
|
|
64
|
+
Handling errors with exceptions can lead to "hidden" control flows. `ripthrow` forces you to acknowledge potential failures, leading to more resilient applications. Whether you are parsing JSON, fetching data, or performing complex logic, `ripthrow` ensures your error handling is as clean as your success path.
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ripthrow",
|
|
3
|
+
"module": "src/index.ts",
|
|
4
|
+
"version": "2.0.0",
|
|
5
|
+
"description": "Zero-overhead, type-safe error handling for TypeScript.",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"result",
|
|
8
|
+
"error-handling",
|
|
9
|
+
"typescript",
|
|
10
|
+
"functional",
|
|
11
|
+
"type-safe"
|
|
12
|
+
],
|
|
13
|
+
"author": "Sir Cesarium",
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/MechanicalLabs/ripthrow.git"
|
|
18
|
+
},
|
|
19
|
+
"sideEffects": false,
|
|
20
|
+
"main": "./src/index.ts",
|
|
21
|
+
"types": "./src/index.ts",
|
|
22
|
+
"files": [
|
|
23
|
+
"src",
|
|
24
|
+
"!src/**/*.test.ts"
|
|
25
|
+
],
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public"
|
|
28
|
+
},
|
|
29
|
+
"type": "module",
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@biomejs/biome": "2.4.14",
|
|
32
|
+
"@types/bun": "latest",
|
|
33
|
+
"@types/node": "^25.6.1",
|
|
34
|
+
"knip": "^6.12.1",
|
|
35
|
+
"typedoc": "^0.28.19",
|
|
36
|
+
"typedoc-plugin-markdown": "^4.11.0"
|
|
37
|
+
},
|
|
38
|
+
"peerDependencies": {
|
|
39
|
+
"typescript": "^6.0.3"
|
|
40
|
+
},
|
|
41
|
+
"scripts": {
|
|
42
|
+
"knip": "knip",
|
|
43
|
+
"lint": "biome check .",
|
|
44
|
+
"format": "biome format --write .",
|
|
45
|
+
"docs": "typedoc"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { Result } from "../types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Matches a Result against different handlers for Ok and Err cases.
|
|
5
|
+
* This pattern forces handling both success and failure states, similar to Rust's match.
|
|
6
|
+
*
|
|
7
|
+
* @template T The type of the success value.
|
|
8
|
+
* @template E The type of the error value.
|
|
9
|
+
* @template R The type of the return value from the handlers.
|
|
10
|
+
* @param result The Result to match.
|
|
11
|
+
* @param handlers An object with `ok` and `err` functions.
|
|
12
|
+
* @returns The result of the appropriate handler.
|
|
13
|
+
*
|
|
14
|
+
* @category Consumers
|
|
15
|
+
* @see unwrapOr
|
|
16
|
+
* @example
|
|
17
|
+
* const output = match(res, {
|
|
18
|
+
* ok: (val) => `Success: ${val}`,
|
|
19
|
+
* err: (err) => `Failure: ${err}`
|
|
20
|
+
* });
|
|
21
|
+
*/
|
|
22
|
+
export function match<T, E, R>(
|
|
23
|
+
result: Result<T, E>,
|
|
24
|
+
handlers: {
|
|
25
|
+
ok: (value: T) => R;
|
|
26
|
+
err: (error: E) => R;
|
|
27
|
+
},
|
|
28
|
+
): R {
|
|
29
|
+
if (result.ok) {
|
|
30
|
+
return handlers.ok(result.value);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return handlers.err(result.error);
|
|
34
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Result } from "../types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Unwraps a Result, returning the contained value if it's Ok, or a default value if it's Err.
|
|
5
|
+
*
|
|
6
|
+
* @template T The type of the success value.
|
|
7
|
+
* @template E The type of the error value.
|
|
8
|
+
* @param result The Result to unwrap.
|
|
9
|
+
* @param defaultValue The value to return if the Result is an Err.
|
|
10
|
+
* @returns The contained value if Ok, otherwise the default value.
|
|
11
|
+
*
|
|
12
|
+
* @category Consumers
|
|
13
|
+
* @see match
|
|
14
|
+
* @example
|
|
15
|
+
* const res = Err("error");
|
|
16
|
+
* const val = unwrapOr(res, "default"); // "default"
|
|
17
|
+
*/
|
|
18
|
+
export function unwrapOr<T, E>(result: Result<T, E>, defaultValue: T): T {
|
|
19
|
+
if (result.ok) {
|
|
20
|
+
return result.value;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return defaultValue;
|
|
24
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Result } from "../types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Unwraps a Result, returning the success value or throwing the error.
|
|
5
|
+
*
|
|
6
|
+
* @template T The type of the success value.
|
|
7
|
+
* @template E The type of the error value.
|
|
8
|
+
* @param result The Result to unwrap.
|
|
9
|
+
* @returns The success value.
|
|
10
|
+
* @throws The error contained in the Result if it's an Err.
|
|
11
|
+
*
|
|
12
|
+
* @category Consumers
|
|
13
|
+
* @see unwrapOr
|
|
14
|
+
* @example
|
|
15
|
+
* const val = unwrap(Ok(42)); // 42
|
|
16
|
+
* const val = unwrap(Err("failed")); // throws "failed"
|
|
17
|
+
*/
|
|
18
|
+
export function unwrap<T, E>(result: Result<T, E>): T {
|
|
19
|
+
if (result.ok) {
|
|
20
|
+
return result.value;
|
|
21
|
+
}
|
|
22
|
+
throw result.error;
|
|
23
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Report, type ReportOptions } from "../report";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Creates a new Report instance.
|
|
5
|
+
* Useful for generating structured errors to be wrapped in an Err result.
|
|
6
|
+
*
|
|
7
|
+
* @param message The error message.
|
|
8
|
+
* @param options Additional report options.
|
|
9
|
+
* @returns A new Report instance.
|
|
10
|
+
*
|
|
11
|
+
* @category Factories
|
|
12
|
+
* @example
|
|
13
|
+
* return Err(bail("Permission denied", { help: "Check your API token" }));
|
|
14
|
+
*/
|
|
15
|
+
export function bail(message: string, options?: ReportOptions): Report {
|
|
16
|
+
return new Report(message, options);
|
|
17
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Result } from "../types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Constructs an error Result containing the given error.
|
|
5
|
+
*
|
|
6
|
+
* @template T The type of the success value.
|
|
7
|
+
* @template E The type of the error value.
|
|
8
|
+
* @param error The error to wrap in an Err result.
|
|
9
|
+
* @returns A failed Result object `{ ok: false, error: E }`.
|
|
10
|
+
*
|
|
11
|
+
* @category Factories
|
|
12
|
+
* @see Ok
|
|
13
|
+
* @example
|
|
14
|
+
* const res = Err("Something went wrong");
|
|
15
|
+
* if (!res.ok) console.error(res.error); // "Something went wrong"
|
|
16
|
+
*/
|
|
17
|
+
export function Err<T, E>(error: E): Result<T, E> {
|
|
18
|
+
return { ok: false, error };
|
|
19
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Result } from "../types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A type guard that checks if a Result is an Err.
|
|
5
|
+
*
|
|
6
|
+
* @template T The type of the success value.
|
|
7
|
+
* @template E The type of the error value.
|
|
8
|
+
* @param result The Result to check.
|
|
9
|
+
* @returns `true` if the result is an Err, `false` otherwise.
|
|
10
|
+
*
|
|
11
|
+
* @category Factories
|
|
12
|
+
* @example
|
|
13
|
+
* if (isErr(res)) {
|
|
14
|
+
* console.error(res.error); // res.error is typed as E
|
|
15
|
+
* }
|
|
16
|
+
*/
|
|
17
|
+
export function isErr<T, E>(result: Result<T, E>): result is { ok: false; error: E } {
|
|
18
|
+
return !result.ok;
|
|
19
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Result } from "../types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A type guard that checks if a Result is an Ok.
|
|
5
|
+
*
|
|
6
|
+
* @template T The type of the success value.
|
|
7
|
+
* @template E The type of the error value.
|
|
8
|
+
* @param result The Result to check.
|
|
9
|
+
* @returns `true` if the result is an Ok, `false` otherwise.
|
|
10
|
+
*
|
|
11
|
+
* @category Factories
|
|
12
|
+
* @example
|
|
13
|
+
* if (isOk(res)) {
|
|
14
|
+
* console.log(res.value); // res.value is typed as T
|
|
15
|
+
* }
|
|
16
|
+
*/
|
|
17
|
+
export function isOk<T, E>(result: Result<T, E>): result is { ok: true; value: T } {
|
|
18
|
+
return result.ok;
|
|
19
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Result } from "../types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Constructs a successful Result containing the given value.
|
|
5
|
+
*
|
|
6
|
+
* @template T The type of the success value.
|
|
7
|
+
* @template E The type of the error value.
|
|
8
|
+
* @param value The value to wrap in an Ok result.
|
|
9
|
+
* @returns A successful Result object `{ ok: true, value: T }`.
|
|
10
|
+
*
|
|
11
|
+
* @category Factories
|
|
12
|
+
* @see Err
|
|
13
|
+
* @example
|
|
14
|
+
* const res = Ok(42);
|
|
15
|
+
* if (res.ok) console.log(res.value); // 42
|
|
16
|
+
*/
|
|
17
|
+
export function Ok<T = void, E = unknown>(value?: T): Result<T, E> {
|
|
18
|
+
return { ok: true, value: value as T };
|
|
19
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Result } from "../types";
|
|
2
|
+
import { Err } from "./err";
|
|
3
|
+
import { Ok } from "./ok";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Converts a Promise into a Result.
|
|
7
|
+
* If the promise resolves, it returns an Ok result with the data.
|
|
8
|
+
* If the promise rejects, it captures the reason and returns an Err result.
|
|
9
|
+
*
|
|
10
|
+
* @template T The type of the success value.
|
|
11
|
+
* @template E The type of the error value. Defaults to Error.
|
|
12
|
+
* @param promise The Promise to convert.
|
|
13
|
+
* @returns A Promise resolving to an Ok result with the data, or an Err result with the error.
|
|
14
|
+
*
|
|
15
|
+
* @category Factories
|
|
16
|
+
* @see safe
|
|
17
|
+
* @example
|
|
18
|
+
* const res = await safeAsync(fetch("https://api.example.com"));
|
|
19
|
+
* if (res.ok) console.log(await res.value.json());
|
|
20
|
+
*/
|
|
21
|
+
export async function safeAsync<T, E = Error>(promise: Promise<T>): Promise<Result<T, E>> {
|
|
22
|
+
try {
|
|
23
|
+
const data = await promise;
|
|
24
|
+
return Ok(data);
|
|
25
|
+
} catch (e) {
|
|
26
|
+
return Err(e as E);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Result } from "../types";
|
|
2
|
+
import { Err } from "./err";
|
|
3
|
+
import { Ok } from "./ok";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Executes a synchronous function and wraps its return value in an Ok result.
|
|
7
|
+
* If the function throws an error, it captures it and wraps it in an Err result.
|
|
8
|
+
*
|
|
9
|
+
* @template T The type of the success value.
|
|
10
|
+
* @template E The type of the error value. Defaults to Error.
|
|
11
|
+
* @param fn The function to execute.
|
|
12
|
+
* @returns An Ok result with the return value, or an Err result with the thrown error.
|
|
13
|
+
*
|
|
14
|
+
* @category Factories
|
|
15
|
+
* @see safeAsync
|
|
16
|
+
* @example
|
|
17
|
+
* const res = safe(() => JSON.parse('{"valid": true}'));
|
|
18
|
+
* if (res.ok) console.log(res.value);
|
|
19
|
+
*/
|
|
20
|
+
export function safe<T, E = Error>(fn: () => T): Result<T, E> {
|
|
21
|
+
try {
|
|
22
|
+
return Ok(fn());
|
|
23
|
+
} catch (e) {
|
|
24
|
+
return Err(e as E);
|
|
25
|
+
}
|
|
26
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ripthrow: A tiny, type-safe Result type for TypeScript.
|
|
3
|
+
* Inspired by Rust's Result and the proposed Error Handling Operator (?=).
|
|
4
|
+
*
|
|
5
|
+
* This library provides a structured way to handle success and failure
|
|
6
|
+
* without relying on exceptions, promoting safer and more predictable code.
|
|
7
|
+
*
|
|
8
|
+
* @module ripthrow
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export * from "./consumers";
|
|
12
|
+
export * from "./factories";
|
|
13
|
+
export * from "./operators";
|
|
14
|
+
export * from "./pattern";
|
|
15
|
+
export * from "./report";
|
|
16
|
+
export * from "./result-builder";
|
|
17
|
+
export * from "./result-builder-async";
|
|
18
|
+
export type * from "./types";
|
|
19
|
+
export * from "./utils";
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { Result } from "../types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Chains a sequence of operations that may fail, short-circuiting on the first error.
|
|
5
|
+
* If the Result is an Ok, it applies the function to the value and returns its Result.
|
|
6
|
+
* If the Result is an Err, it returns the original error without executing the function.
|
|
7
|
+
*
|
|
8
|
+
* Also known as `flatMap` or `bind` in other functional contexts.
|
|
9
|
+
*
|
|
10
|
+
* @template T The type of the success value.
|
|
11
|
+
* @template E The type of the error value.
|
|
12
|
+
* @template R The type of the new success value.
|
|
13
|
+
* @param result The initial Result.
|
|
14
|
+
* @param fn The function to apply to the success value if it's Ok.
|
|
15
|
+
* @returns A new Result representing the chained operation.
|
|
16
|
+
*
|
|
17
|
+
* @category Operators
|
|
18
|
+
* @see map
|
|
19
|
+
* @example
|
|
20
|
+
* const res = Ok("user_id");
|
|
21
|
+
* const user = andThen(res, fetchUser); // fetchUser returns a Result
|
|
22
|
+
*/
|
|
23
|
+
export function andThen<T, E, R>(
|
|
24
|
+
result: Result<T, E>,
|
|
25
|
+
fn: (value: T) => Result<R, E>,
|
|
26
|
+
): Result<R, E> {
|
|
27
|
+
if (result.ok) {
|
|
28
|
+
return fn(result.value);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return result;
|
|
32
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Report } from "../report";
|
|
2
|
+
import type { Result } from "../types/result";
|
|
3
|
+
import { mapErr } from "./map-err";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Attaches context to a Result's error, converting it to a Report if it isn't one already.
|
|
7
|
+
*
|
|
8
|
+
* @template T The type of the success value.
|
|
9
|
+
* @template E The type of the error value.
|
|
10
|
+
* @param result The Result to attach context to.
|
|
11
|
+
* @param message The context message.
|
|
12
|
+
* @param help An optional help message.
|
|
13
|
+
* @returns A Result with the same success value or a Report as the error.
|
|
14
|
+
*
|
|
15
|
+
* @category Operators
|
|
16
|
+
* @example
|
|
17
|
+
* const res = context(safe(() => JSON.parse(data)), "Failed to parse config");
|
|
18
|
+
*/
|
|
19
|
+
export function context<T, E>(
|
|
20
|
+
result: Result<T, E>,
|
|
21
|
+
message: string,
|
|
22
|
+
help?: string,
|
|
23
|
+
): Result<T, Report> {
|
|
24
|
+
return mapErr(result, (err) => Report.from(err, message, { help }));
|
|
25
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Err } from "../factories/err";
|
|
2
|
+
import type { Result } from "../types";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Maps a Result's error value using the provided function, leaving successes unchanged.
|
|
6
|
+
* If the Result is an Ok, the function is not executed and the original success value is returned.
|
|
7
|
+
*
|
|
8
|
+
* @template T The type of the success value.
|
|
9
|
+
* @template E The type of the error value.
|
|
10
|
+
* @template F The type of the new error value.
|
|
11
|
+
* @param result The Result to map.
|
|
12
|
+
* @param fn The function to apply to the error value if it's Err.
|
|
13
|
+
* @returns A new Result with the mapped error value or the original success.
|
|
14
|
+
*
|
|
15
|
+
* @category Operators
|
|
16
|
+
* @see map
|
|
17
|
+
* @see orElse
|
|
18
|
+
* @example
|
|
19
|
+
* const res = Err("not found");
|
|
20
|
+
* const wrapped = mapErr(res, (e) => new Error(e)); // Err(Error("not found"))
|
|
21
|
+
*/
|
|
22
|
+
export function mapErr<T, E, F>(result: Result<T, E>, fn: (error: E) => F): Result<T, F> {
|
|
23
|
+
if (!result.ok) {
|
|
24
|
+
return Err(fn(result.error));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return result;
|
|
28
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Ok } from "../factories/ok";
|
|
2
|
+
import type { Result } from "../types";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Maps a Result's success value using the provided function, leaving errors unchanged.
|
|
6
|
+
* If the Result is an Err, the function is not executed and the original error is returned.
|
|
7
|
+
*
|
|
8
|
+
* @template T The type of the success value.
|
|
9
|
+
* @template E The type of the error value.
|
|
10
|
+
* @template R The type of the new success value.
|
|
11
|
+
* @param result The Result to map.
|
|
12
|
+
* @param fn The function to apply to the success value if it's Ok.
|
|
13
|
+
* @returns A new Result with the mapped success value or the original error.
|
|
14
|
+
*
|
|
15
|
+
* @category Operators
|
|
16
|
+
* @see mapErr
|
|
17
|
+
* @see andThen
|
|
18
|
+
* @example
|
|
19
|
+
* const res = Ok(1);
|
|
20
|
+
* const doubled = map(res, (n) => n * 2); // Ok(2)
|
|
21
|
+
*/
|
|
22
|
+
export function map<T, E, R>(result: Result<T, E>, fn: (value: T) => R): Result<R, E> {
|
|
23
|
+
if (result.ok) {
|
|
24
|
+
return Ok(fn(result.value));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return result;
|
|
28
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Result } from "../types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Chains a sequence of operations that may fail, allowing recovery from errors.
|
|
5
|
+
* If the Result is an Err, it applies the function to the error and returns its Result.
|
|
6
|
+
* If the Result is an Ok, it returns the original success value without executing the function.
|
|
7
|
+
*
|
|
8
|
+
* @template T The type of the success value.
|
|
9
|
+
* @template E The type of the error value.
|
|
10
|
+
* @template F The type of the new error value.
|
|
11
|
+
* @param result The initial Result.
|
|
12
|
+
* @param fn The function to apply to the error value if it's Err.
|
|
13
|
+
* @returns A new Result representing the chained operation with potential recovery.
|
|
14
|
+
*
|
|
15
|
+
* @category Operators
|
|
16
|
+
* @see mapErr
|
|
17
|
+
* @example
|
|
18
|
+
* const res = Err("failed");
|
|
19
|
+
* const recovered = orElse(res, () => Ok("default")); // Ok("default")
|
|
20
|
+
*/
|
|
21
|
+
export function orElse<T, E, F>(
|
|
22
|
+
result: Result<T, E>,
|
|
23
|
+
fn: (error: E) => Result<T, F>,
|
|
24
|
+
): Result<T, F> {
|
|
25
|
+
if (!result.ok) {
|
|
26
|
+
return fn(result.error);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return result;
|
|
30
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Result } from "../types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Executes a side effect function if the Result is an Err.
|
|
5
|
+
* The original Result is returned unchanged.
|
|
6
|
+
*
|
|
7
|
+
* @template T The type of the success value.
|
|
8
|
+
* @template E The type of the error value.
|
|
9
|
+
* @param result The Result to tap into.
|
|
10
|
+
* @param fn The function to execute with the error value.
|
|
11
|
+
* @returns The original Result unchanged.
|
|
12
|
+
*
|
|
13
|
+
* @category Operators
|
|
14
|
+
* @see tap
|
|
15
|
+
* @example
|
|
16
|
+
* safe(() => doSomething()).tapErr(err => logger.error(err))
|
|
17
|
+
*/
|
|
18
|
+
export function tapErr<T, E>(result: Result<T, E>, fn: (error: E) => void): Result<T, E> {
|
|
19
|
+
if (!result.ok) {
|
|
20
|
+
fn(result.error);
|
|
21
|
+
}
|
|
22
|
+
return result;
|
|
23
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Result } from "../types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Executes a side effect function if the Result is an Ok.
|
|
5
|
+
* The original Result is returned unchanged.
|
|
6
|
+
*
|
|
7
|
+
* @template T The type of the success value.
|
|
8
|
+
* @template E The type of the error value.
|
|
9
|
+
* @param result The Result to tap into.
|
|
10
|
+
* @param fn The function to execute with the success value.
|
|
11
|
+
* @returns The original Result unchanged.
|
|
12
|
+
*
|
|
13
|
+
* @category Operators
|
|
14
|
+
* @see tapErr
|
|
15
|
+
* @example
|
|
16
|
+
* map(res, val => val + 1).tap(val => console.log(val))
|
|
17
|
+
*/
|
|
18
|
+
export function tap<T, E>(result: Result<T, E>, fn: (value: T) => void): Result<T, E> {
|
|
19
|
+
if (result.ok) {
|
|
20
|
+
fn(result.value);
|
|
21
|
+
}
|
|
22
|
+
return result;
|
|
23
|
+
}
|
package/src/pattern.ts
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
// biome-ignore lint/nursery/noExcessiveClassesPerFile: _Error is local inside createError
|
|
2
|
+
import type { Result } from "./types/result";
|
|
3
|
+
|
|
4
|
+
const $class = Symbol("error-class");
|
|
5
|
+
const noMatch = Symbol("no-match");
|
|
6
|
+
|
|
7
|
+
interface TypedError<A extends unknown[], N extends string> extends Error {
|
|
8
|
+
readonly args: A;
|
|
9
|
+
readonly help?: string;
|
|
10
|
+
readonly name: N;
|
|
11
|
+
readonly kind: N;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface HandlerEntry {
|
|
15
|
+
execute: (err: unknown) => unknown;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface ExternalErrFactory<C extends new (...args: never[]) => Error> {
|
|
19
|
+
(...args: ConstructorParameters<C>): InstanceType<C>;
|
|
20
|
+
readonly [$class]: C;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface ErrDefEntry {
|
|
24
|
+
// biome-ignore lint/suspicious/noExplicitAny: required for Parameters<>
|
|
25
|
+
message: (...args: any[]) => string;
|
|
26
|
+
// biome-ignore lint/suspicious/noExplicitAny: required for Parameters<>
|
|
27
|
+
help?: (...args: any[]) => string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
type ErrDefMap = Record<string, ErrDefEntry>;
|
|
31
|
+
|
|
32
|
+
declare const exhaustiveCheck: unique symbol;
|
|
33
|
+
type TypedKinds<E> = E extends TypedError<unknown[], infer K> ? K : never;
|
|
34
|
+
type AllTypedKindsHandled<E, Handled extends string> = TypedKinds<E> extends Handled ? true : false;
|
|
35
|
+
|
|
36
|
+
type ErrorFactories<T extends ErrDefMap> = {
|
|
37
|
+
[K in keyof T & string]: ErrFactory<Parameters<T[K]["message"]>, K>;
|
|
38
|
+
} & {
|
|
39
|
+
readonly _type: {
|
|
40
|
+
[K in keyof T & string]: TypedError<Parameters<T[K]["message"]>, K>;
|
|
41
|
+
}[keyof T & string];
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export function createErrors<T extends ErrDefMap>(defs: T): ErrorFactories<T> {
|
|
45
|
+
const result: Record<string, ErrFactory<unknown[], string>> = {};
|
|
46
|
+
for (const [name, def] of Object.entries(defs)) {
|
|
47
|
+
result[name] = createError(name, def.message, def.help);
|
|
48
|
+
}
|
|
49
|
+
return result as unknown as ErrorFactories<T>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function createError<A extends unknown[], N extends string>(
|
|
53
|
+
name: N,
|
|
54
|
+
message: (...args: A) => string,
|
|
55
|
+
help?: (...args: A) => string,
|
|
56
|
+
): ErrFactory<A, N> {
|
|
57
|
+
class _Error extends Error {
|
|
58
|
+
override readonly name: N;
|
|
59
|
+
readonly args: A;
|
|
60
|
+
readonly help: string | undefined;
|
|
61
|
+
readonly kind: N = name;
|
|
62
|
+
|
|
63
|
+
constructor(...args: A) {
|
|
64
|
+
super(message(...args));
|
|
65
|
+
this.name = name;
|
|
66
|
+
this.args = args;
|
|
67
|
+
if (help) {
|
|
68
|
+
this.help = help(...args);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
Object.defineProperty(_Error, "name", { value: name });
|
|
73
|
+
|
|
74
|
+
const factory = (...args: A): TypedError<A, N> =>
|
|
75
|
+
new _Error(...args) as unknown as TypedError<A, N>;
|
|
76
|
+
Object.defineProperty(factory, $class, { value: _Error });
|
|
77
|
+
return factory as unknown as ErrFactory<A, N>;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function wrapError<C extends new (...args: never[]) => Error>(
|
|
81
|
+
cls: C,
|
|
82
|
+
): ExternalErrFactory<C> {
|
|
83
|
+
const factory = (...args: ConstructorParameters<C>): Error => new cls(...args);
|
|
84
|
+
Object.defineProperty(factory, $class, { value: cls });
|
|
85
|
+
return factory as unknown as ExternalErrFactory<C>;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export class MatchErrBuilder<T, E, R, Handled extends string = never> {
|
|
89
|
+
private readonly result: Result<T, E>;
|
|
90
|
+
private readonly handlers: HandlerEntry[] = [];
|
|
91
|
+
|
|
92
|
+
constructor(result: Result<T, E>) {
|
|
93
|
+
this.result = result;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
on<A extends unknown[], N extends string, HandlerResult>(
|
|
97
|
+
def: ErrFactory<A, N>,
|
|
98
|
+
handler: (err: TypedError<A, N>) => HandlerResult,
|
|
99
|
+
): MatchErrBuilder<T, E, R | HandlerResult, Handled | N>;
|
|
100
|
+
|
|
101
|
+
on<C extends new (...args: never[]) => Error, HandlerResult>(
|
|
102
|
+
def: ExternalErrFactory<C>,
|
|
103
|
+
handler: (err: InstanceType<C>) => HandlerResult,
|
|
104
|
+
): MatchErrBuilder<T, E, R | HandlerResult, Handled>;
|
|
105
|
+
|
|
106
|
+
on(def: unknown, handler: unknown): this {
|
|
107
|
+
const _class = (def as { readonly [$class]: new (...args: never[]) => Error })[$class];
|
|
108
|
+
this.handlers.push({
|
|
109
|
+
execute: (err: unknown) => {
|
|
110
|
+
if ((err as Error) instanceof _class) {
|
|
111
|
+
return (handler as (err: unknown) => unknown)(err);
|
|
112
|
+
}
|
|
113
|
+
return noMatch;
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
return this;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
otherwise(fallback: (err: E) => R): T | R {
|
|
120
|
+
return this.execute(fallback);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
exhaustive(): AllTypedKindsHandled<E, Handled> extends true
|
|
124
|
+
? T | R
|
|
125
|
+
: { readonly [exhaustiveCheck]: typeof exhaustiveCheck } {
|
|
126
|
+
return this.execute((err) => {
|
|
127
|
+
throw err;
|
|
128
|
+
}) as never;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private execute(fallback: (err: E) => R): T | R {
|
|
132
|
+
if (this.result.ok) {
|
|
133
|
+
return this.result.value;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// biome-ignore lint/nursery/useDestructuring: discriminated union prevents destructuring
|
|
137
|
+
const error = this.result.error;
|
|
138
|
+
|
|
139
|
+
for (const entry of this.handlers) {
|
|
140
|
+
const result = entry.execute(error);
|
|
141
|
+
if (result !== noMatch) {
|
|
142
|
+
return result as R;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return fallback(error);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function matchErr<T, E>(result: Result<T, E>): MatchErrBuilder<T, E, never, never> {
|
|
151
|
+
return new MatchErrBuilder(result);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export interface ErrFactory<A extends unknown[], N extends string> {
|
|
155
|
+
(...args: A): TypedError<A, N>;
|
|
156
|
+
readonly [$class]: new (...args: A) => TypedError<A, N>;
|
|
157
|
+
}
|
package/src/report.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Options for creating a new Report.
|
|
3
|
+
*
|
|
4
|
+
* @category Error Handling
|
|
5
|
+
*/
|
|
6
|
+
export interface ReportOptions {
|
|
7
|
+
/** The underlying cause of the error. */
|
|
8
|
+
cause?: unknown;
|
|
9
|
+
/** A helpful message for the user on how to resolve the error. */
|
|
10
|
+
help?: string | undefined;
|
|
11
|
+
/** Additional key-value pairs to provide more context. */
|
|
12
|
+
context?: Record<string, unknown> | undefined;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* A structured error class that supports causes, help messages, and context.
|
|
17
|
+
* Inspired by Rust's `anyhow` or `eyre`.
|
|
18
|
+
*
|
|
19
|
+
* @category Error Handling
|
|
20
|
+
*/
|
|
21
|
+
export class Report extends Error {
|
|
22
|
+
/** A helpful message for the user on how to resolve the error. */
|
|
23
|
+
readonly help?: string | undefined;
|
|
24
|
+
/** Additional key-value pairs to provide more context. */
|
|
25
|
+
readonly context?: Record<string, unknown> | undefined;
|
|
26
|
+
|
|
27
|
+
constructor(message: string, options: ReportOptions = {}) {
|
|
28
|
+
super(message);
|
|
29
|
+
this.name = "Report";
|
|
30
|
+
this.help = options.help;
|
|
31
|
+
this.context = options.context;
|
|
32
|
+
if (options.cause) {
|
|
33
|
+
this.cause = options.cause;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Creates a Report from an unknown error.
|
|
39
|
+
* If the error is already a Report and no new information is provided, it returns it as is.
|
|
40
|
+
*
|
|
41
|
+
* @param err The original error.
|
|
42
|
+
* @param message An optional new message.
|
|
43
|
+
* @param options Additional options.
|
|
44
|
+
* @returns A Report instance.
|
|
45
|
+
*/
|
|
46
|
+
static from(err: unknown, message?: string, options: ReportOptions = {}): Report {
|
|
47
|
+
if (err instanceof Report && !message && !options.help && !options.context) {
|
|
48
|
+
return err;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
let baseMessage: string;
|
|
52
|
+
if (message) {
|
|
53
|
+
baseMessage = message;
|
|
54
|
+
} else if (err instanceof Error) {
|
|
55
|
+
baseMessage = err.message;
|
|
56
|
+
} else {
|
|
57
|
+
baseMessage = String(err);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return new Report(baseMessage, { cause: err, ...options });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { match } from "./consumers/match";
|
|
2
|
+
import { unwrap } from "./consumers/unwrap";
|
|
3
|
+
import { unwrapOr } from "./consumers/unwrap-or";
|
|
4
|
+
import { Err } from "./factories/err";
|
|
5
|
+
import { isErr } from "./factories/is-err";
|
|
6
|
+
import { isOk } from "./factories/is-ok";
|
|
7
|
+
import { Ok } from "./factories/ok";
|
|
8
|
+
import { safeAsync } from "./factories/safe-async";
|
|
9
|
+
import { context as contextOp } from "./operators/context";
|
|
10
|
+
import { map } from "./operators/map";
|
|
11
|
+
import { mapErr } from "./operators/map-err";
|
|
12
|
+
import { tap } from "./operators/tap";
|
|
13
|
+
import { tapErr } from "./operators/tap-err";
|
|
14
|
+
import type { Report } from "./report";
|
|
15
|
+
import type { AsyncResult, Result } from "./types/result";
|
|
16
|
+
|
|
17
|
+
export class AsyncResultBuilder<T, E> {
|
|
18
|
+
private readonly _promise: AsyncResult<T, E>;
|
|
19
|
+
|
|
20
|
+
constructor(promise: AsyncResult<T, E>) {
|
|
21
|
+
this._promise = promise;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
static ok<U = void, F = unknown>(value?: U): AsyncResultBuilder<U, F> {
|
|
25
|
+
return new AsyncResultBuilder(Promise.resolve(Ok(value) as Result<U, F>));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
static err<U, F>(error: F): AsyncResultBuilder<U, F> {
|
|
29
|
+
return new AsyncResultBuilder(Promise.resolve(Err(error) as Result<U, F>));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
static safeAsync<U, F = Error>(promise: Promise<U>): AsyncResultBuilder<U, F> {
|
|
33
|
+
return new AsyncResultBuilder(safeAsync(promise) as AsyncResult<U, F>);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
static all<U, F>(results: AsyncResult<U, F>[]): AsyncResultBuilder<U[], F> {
|
|
37
|
+
return new AsyncResultBuilder(
|
|
38
|
+
Promise.all(results).then((resolved) => {
|
|
39
|
+
const values: U[] = [];
|
|
40
|
+
for (const res of resolved) {
|
|
41
|
+
if (!res.ok) {
|
|
42
|
+
return Err(res.error);
|
|
43
|
+
}
|
|
44
|
+
values.push(res.value);
|
|
45
|
+
}
|
|
46
|
+
return Ok(values);
|
|
47
|
+
}),
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
static any<U, F>(results: AsyncResult<U, F>[]): AsyncResultBuilder<U, F> {
|
|
52
|
+
if (results.length === 0) {
|
|
53
|
+
return new AsyncResultBuilder(
|
|
54
|
+
Promise.resolve(Err(new Error("any() called with an empty array") as unknown as F)),
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return new AsyncResultBuilder(
|
|
59
|
+
Promise.all(results).then((resolved) => {
|
|
60
|
+
let lastErr: F | undefined;
|
|
61
|
+
for (const res of resolved) {
|
|
62
|
+
if (res.ok) {
|
|
63
|
+
return res;
|
|
64
|
+
}
|
|
65
|
+
lastErr = res.error;
|
|
66
|
+
}
|
|
67
|
+
return Err(lastErr as F);
|
|
68
|
+
}),
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
get result(): AsyncResult<T, E> {
|
|
73
|
+
return this._promise;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
get isOk(): Promise<boolean> {
|
|
77
|
+
return this._promise.then(isOk);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
get isErr(): Promise<boolean> {
|
|
81
|
+
return this._promise.then(isErr);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
map<R>(fn: (value: T) => R): AsyncResultBuilder<R, E> {
|
|
85
|
+
return new AsyncResultBuilder(this._promise.then((r) => map(r, fn)));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
mapErr<F>(fn: (error: E) => F): AsyncResultBuilder<T, F> {
|
|
89
|
+
return new AsyncResultBuilder(this._promise.then((r) => mapErr(r, fn)));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
andThen<R>(fn: (value: T) => Result<R, E> | AsyncResult<R, E>): AsyncResultBuilder<R, E> {
|
|
93
|
+
return new AsyncResultBuilder(
|
|
94
|
+
this._promise.then((r) => {
|
|
95
|
+
if (!r.ok) {
|
|
96
|
+
return r;
|
|
97
|
+
}
|
|
98
|
+
return fn(r.value);
|
|
99
|
+
}),
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
orElse<F>(fn: (error: E) => Result<T, F> | AsyncResult<T, F>): AsyncResultBuilder<T, F> {
|
|
104
|
+
return new AsyncResultBuilder(
|
|
105
|
+
this._promise.then((r) => {
|
|
106
|
+
if (r.ok) {
|
|
107
|
+
return r;
|
|
108
|
+
}
|
|
109
|
+
return fn(r.error);
|
|
110
|
+
}),
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
tap(fn: (value: T) => void): AsyncResultBuilder<T, E> {
|
|
115
|
+
return new AsyncResultBuilder(this._promise.then((r) => tap(r, fn)));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
tapErr(fn: (error: E) => void): AsyncResultBuilder<T, E> {
|
|
119
|
+
return new AsyncResultBuilder(this._promise.then((r) => tapErr(r, fn)));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
context(message: string, help?: string): AsyncResultBuilder<T, Report> {
|
|
123
|
+
return new AsyncResultBuilder(this._promise.then((r) => contextOp(r, message, help)));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
match<R>(handlers: { ok: (value: T) => R; err: (error: E) => R }): Promise<R> {
|
|
127
|
+
return this._promise.then((r) => match(r, handlers));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
unwrapOr(defaultValue: T): Promise<T> {
|
|
131
|
+
return this._promise.then((r) => unwrapOr(r, defaultValue));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
unwrap(): Promise<T> {
|
|
135
|
+
return this._promise.then((r) => unwrap(r));
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function buildAsync<T, E>(promise: AsyncResult<T, E>): AsyncResultBuilder<T, E> {
|
|
140
|
+
return new AsyncResultBuilder(promise);
|
|
141
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { match } from "./consumers/match";
|
|
2
|
+
import { unwrap } from "./consumers/unwrap";
|
|
3
|
+
import { unwrapOr } from "./consumers/unwrap-or";
|
|
4
|
+
import { isErr } from "./factories/is-err";
|
|
5
|
+
import { isOk } from "./factories/is-ok";
|
|
6
|
+
import { andThen } from "./operators/and-then";
|
|
7
|
+
import { context as contextOp } from "./operators/context";
|
|
8
|
+
import { map } from "./operators/map";
|
|
9
|
+
import { mapErr } from "./operators/map-err";
|
|
10
|
+
import { orElse } from "./operators/or-else";
|
|
11
|
+
import { tap } from "./operators/tap";
|
|
12
|
+
import { tapErr } from "./operators/tap-err";
|
|
13
|
+
import type { Report } from "./report";
|
|
14
|
+
import type { Result } from "./types/result";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* A fluent wrapper around the Result type that allows for method chaining.
|
|
18
|
+
*
|
|
19
|
+
* @template T The type of the success value.
|
|
20
|
+
* @template E The type of the error value.
|
|
21
|
+
*
|
|
22
|
+
* @category Builder
|
|
23
|
+
*/
|
|
24
|
+
export class ResultBuilder<T, E> {
|
|
25
|
+
private readonly _result: Result<T, E>;
|
|
26
|
+
|
|
27
|
+
constructor(result: Result<T, E>) {
|
|
28
|
+
this._result = result;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Constructs a successful Result wrapped in a ResultBuilder.
|
|
33
|
+
*/
|
|
34
|
+
static ok<U = void, F = unknown>(value?: U): ResultBuilder<U, F> {
|
|
35
|
+
return new ResultBuilder({ ok: true, value: value as U });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Constructs an error Result wrapped in a ResultBuilder.
|
|
40
|
+
*/
|
|
41
|
+
static err<U, F>(error: F): ResultBuilder<U, F> {
|
|
42
|
+
return new ResultBuilder({ ok: false, error });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Executes a synchronous function and wraps the result in a ResultBuilder.
|
|
47
|
+
*/
|
|
48
|
+
static safe<U, F = Error>(fn: () => U): ResultBuilder<U, F> {
|
|
49
|
+
try {
|
|
50
|
+
return ResultBuilder.ok(fn());
|
|
51
|
+
} catch (e) {
|
|
52
|
+
return ResultBuilder.err(e as F);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Combines multiple Results into a single ResultBuilder.
|
|
58
|
+
*/
|
|
59
|
+
static all<U, F>(results: Result<U, F>[]): ResultBuilder<U[], F> {
|
|
60
|
+
const values: U[] = [];
|
|
61
|
+
for (const res of results) {
|
|
62
|
+
if (!res.ok) {
|
|
63
|
+
return ResultBuilder.err(res.error);
|
|
64
|
+
}
|
|
65
|
+
values.push(res.value);
|
|
66
|
+
}
|
|
67
|
+
return ResultBuilder.ok(values);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Returns the first Ok Result from an array wrapped in a ResultBuilder.
|
|
72
|
+
*/
|
|
73
|
+
static any<U, F>(results: Result<U, F>[]): ResultBuilder<U, F> {
|
|
74
|
+
if (results.length === 0) {
|
|
75
|
+
return ResultBuilder.err(new Error("any() called with an empty array") as unknown as F);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
let lastErr: F | undefined;
|
|
79
|
+
for (const res of results) {
|
|
80
|
+
if (res.ok) {
|
|
81
|
+
return new ResultBuilder(res);
|
|
82
|
+
}
|
|
83
|
+
lastErr = res.error;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return ResultBuilder.err(lastErr as F);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Returns the underlying raw Result type.
|
|
91
|
+
*/
|
|
92
|
+
get result(): Result<T, E> {
|
|
93
|
+
return this._result;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Returns true if the result is an Ok.
|
|
98
|
+
*/
|
|
99
|
+
get isOk(): boolean {
|
|
100
|
+
return isOk(this._result);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Returns true if the result is an Err.
|
|
105
|
+
*/
|
|
106
|
+
get isErr(): boolean {
|
|
107
|
+
return isErr(this._result);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Maps the success value if the result is Ok.
|
|
112
|
+
*/
|
|
113
|
+
map<R>(fn: (value: T) => R): ResultBuilder<R, E> {
|
|
114
|
+
return new ResultBuilder(map(this._result, fn));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Maps the error value if the result is Err.
|
|
119
|
+
*/
|
|
120
|
+
mapErr<F>(fn: (error: E) => F): ResultBuilder<T, F> {
|
|
121
|
+
return new ResultBuilder(mapErr(this._result, fn));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Chains another operation that returns a Result.
|
|
126
|
+
*/
|
|
127
|
+
andThen<R>(fn: (value: T) => Result<R, E>): ResultBuilder<R, E> {
|
|
128
|
+
return new ResultBuilder(andThen(this._result, fn));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Chains another operation that returns a Result if the current result is an Err.
|
|
133
|
+
*/
|
|
134
|
+
orElse<F>(fn: (error: E) => Result<T, F>): ResultBuilder<T, F> {
|
|
135
|
+
return new ResultBuilder(orElse(this._result, fn));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Executes a side effect if the result is Ok.
|
|
140
|
+
*/
|
|
141
|
+
tap(fn: (value: T) => void): this {
|
|
142
|
+
tap(this._result, fn);
|
|
143
|
+
return this;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Executes a side effect if the result is Err.
|
|
148
|
+
*/
|
|
149
|
+
tapErr(fn: (error: E) => void): this {
|
|
150
|
+
tapErr(this._result, fn);
|
|
151
|
+
return this;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Matches the result against Ok and Err handlers.
|
|
156
|
+
*/
|
|
157
|
+
match<R>(handlers: { ok: (value: T) => R; err: (error: E) => R }): R {
|
|
158
|
+
return match(this._result, handlers);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Unwraps the value or returns a default value.
|
|
163
|
+
*/
|
|
164
|
+
unwrapOr(defaultValue: T): T {
|
|
165
|
+
return unwrapOr(this._result, defaultValue);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Unwraps the value or throws the error.
|
|
170
|
+
*/
|
|
171
|
+
unwrap(): T {
|
|
172
|
+
return unwrap(this._result);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Attaches context to the error if it exists.
|
|
177
|
+
*/
|
|
178
|
+
context(message: string, help?: string): ResultBuilder<T, Report> {
|
|
179
|
+
return new ResultBuilder(contextOp(this._result, message, help));
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Wraps a Result in a ResultBuilder for fluent chaining.
|
|
185
|
+
*
|
|
186
|
+
* @param result The Result to wrap.
|
|
187
|
+
* @returns A ResultBuilder instance.
|
|
188
|
+
*
|
|
189
|
+
* @category Builder
|
|
190
|
+
*/
|
|
191
|
+
export function build<T, E>(result: Result<T, E>): ResultBuilder<T, E> {
|
|
192
|
+
return new ResultBuilder(result);
|
|
193
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type * from "./result";
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A Result type representing either a success (Ok) or an error (Err).
|
|
3
|
+
*
|
|
4
|
+
* @template T The type of the success value.
|
|
5
|
+
* @template E The type of the error value.
|
|
6
|
+
*/
|
|
7
|
+
export type Result<T, E> = { ok: true; value: T } | { ok: false; error: E };
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Represents a Result that is asynchronously resolved.
|
|
11
|
+
*
|
|
12
|
+
* @template T The type of the success value.
|
|
13
|
+
* @template E The type of the error value.
|
|
14
|
+
*/
|
|
15
|
+
export type AsyncResult<T, E> = Promise<Result<T, E>>;
|
package/src/utils/all.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Err } from "../factories/err";
|
|
2
|
+
import { Ok } from "../factories/ok";
|
|
3
|
+
import type { Result } from "../types";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Combines multiple Results into a single Result.
|
|
7
|
+
* If all Results are Ok, returns an Ok with an array of all values.
|
|
8
|
+
* If any Result is an Err, returns the first Err encountered.
|
|
9
|
+
*
|
|
10
|
+
* @template T The type of the success values.
|
|
11
|
+
* @template E The type of the error value.
|
|
12
|
+
* @param results An array of Results to combine.
|
|
13
|
+
* @returns A Result with an array of values or the first error.
|
|
14
|
+
*
|
|
15
|
+
* @category Utilities
|
|
16
|
+
* @see any
|
|
17
|
+
* @example
|
|
18
|
+
* const res = all([Ok(1), Ok(2)]); // Ok([1, 2])
|
|
19
|
+
* const res = all([Ok(1), Err("fail")]); // Err("fail")
|
|
20
|
+
*/
|
|
21
|
+
export function all<T, E>(results: Result<T, E>[]): Result<T[], E> {
|
|
22
|
+
const values: T[] = [];
|
|
23
|
+
for (const res of results) {
|
|
24
|
+
if (!res.ok) {
|
|
25
|
+
return Err(res.error);
|
|
26
|
+
}
|
|
27
|
+
values.push(res.value);
|
|
28
|
+
}
|
|
29
|
+
return Ok(values);
|
|
30
|
+
}
|
package/src/utils/any.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Err } from "../factories/err";
|
|
2
|
+
import type { Result } from "../types";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Returns the first Ok Result from an array of Results.
|
|
6
|
+
* If all Results are Err, returns the last Err encountered (or a default error if empty).
|
|
7
|
+
*
|
|
8
|
+
* @template T The type of the success value.
|
|
9
|
+
* @template E The type of the error value.
|
|
10
|
+
* @param results An array of Results to check.
|
|
11
|
+
* @returns The first Ok result or the last Err.
|
|
12
|
+
*
|
|
13
|
+
* @category Utilities
|
|
14
|
+
* @see all
|
|
15
|
+
* @example
|
|
16
|
+
* const res = any([Err("a"), Ok(1), Ok(2)]); // Ok(1)
|
|
17
|
+
*/
|
|
18
|
+
export function any<T, E>(results: Result<T, E>[]): Result<T, E> {
|
|
19
|
+
if (results.length === 0) {
|
|
20
|
+
return Err(new Error("any() called with an empty array") as unknown as E);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let lastErr: E | undefined;
|
|
24
|
+
for (const res of results) {
|
|
25
|
+
if (res.ok) {
|
|
26
|
+
return res;
|
|
27
|
+
}
|
|
28
|
+
lastErr = res.error;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return Err(lastErr as E);
|
|
32
|
+
}
|