ripthrow 2.0.1 → 2.1.1
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/README.md +112 -0
- package/package.json +4 -2
- package/src/operators/context.ts +16 -1
- package/src/pattern.ts +6 -1
- package/src/result-builder-async.ts +79 -29
- package/src/result-builder.ts +6 -2
- package/src/utils/index.ts +2 -0
- package/src/utils/kind-of.ts +22 -0
- package/src/utils/pipe.ts +25 -0
package/README.md
CHANGED
|
@@ -64,3 +64,115 @@ console.log(isValid); // true
|
|
|
64
64
|
## Why ripthrow?
|
|
65
65
|
|
|
66
66
|
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.
|
|
67
|
+
|
|
68
|
+
## Benchmarks
|
|
69
|
+
|
|
70
|
+
All benchmarks run on Bun 1.3 via [tinybench](https://github.com/tinylibs/tinybench) (`bun run bench`).
|
|
71
|
+
Higher ops/s = faster. Latency is per-operation (lower is better).
|
|
72
|
+
|
|
73
|
+
### Construction
|
|
74
|
+
|
|
75
|
+
| Pattern | ops/s | Latency | vs native |
|
|
76
|
+
|---------|------:|--------:|----------:|
|
|
77
|
+
| `Ok()` | 25,106,258 | 43.7 ns | — |
|
|
78
|
+
| `Err()` | 25,330,770 | 43.5 ns | — |
|
|
79
|
+
| `throw` | 1,182,372 | 1076.0 ns | **22× slower** |
|
|
80
|
+
| `{ ok: true }` manual | 25,110,815 | 43.8 ns | identical |
|
|
81
|
+
|
|
82
|
+
> `Ok`/`Err` are object literals — zero overhead vs writing the union manually.
|
|
83
|
+
> `throw` is the expensive one (Error object creation), not ripthrow.
|
|
84
|
+
|
|
85
|
+
### Wrapping (`safe` vs `try/catch`)
|
|
86
|
+
|
|
87
|
+
| Pattern | ops/s | Latency |
|
|
88
|
+
|---------|------:|--------:|
|
|
89
|
+
| `safe(success)` | 21,364,164 | 54.3 ns |
|
|
90
|
+
| `try/catch` (no throw) | 25,834,736 | 41.5 ns |
|
|
91
|
+
| `safe(throws)` | 809,722 | 1513.3 ns |
|
|
92
|
+
| `try/catch` (throw) | 1,137,303 | 1154.6 ns |
|
|
93
|
+
| `safeAsync(success)` | 3,179,026 | 349.5 ns |
|
|
94
|
+
| `try/catch async` (success) | 3,128,980 | 356.4 ns |
|
|
95
|
+
|
|
96
|
+
> `safe` adds <15 ns vs raw try/catch on the success path.
|
|
97
|
+
> On throws, both are bottlenecked by Error object creation — ripthrow is not the overhead.
|
|
98
|
+
> Async paths are identical (bottleneck is Promise scheduling).
|
|
99
|
+
|
|
100
|
+
### Mapping & Chaining
|
|
101
|
+
|
|
102
|
+
| Pattern | ops/s | Latency |
|
|
103
|
+
|---------|------:|--------:|
|
|
104
|
+
| `map` x5 | 13,822,582 | 90.6 ns |
|
|
105
|
+
| `if/else` x5 (native) | 19,130,552 | 64.6 ns |
|
|
106
|
+
| `andThen` chain | 19,259,304 | 57.7 ns |
|
|
107
|
+
| `orElse` fallback | 21,331,229 | 52.5 ns |
|
|
108
|
+
| builder chain (5 ops) | 18,372,064 | 61.2 ns |
|
|
109
|
+
|
|
110
|
+
> Functional overhead is ~3-5 ns per function call. The builder (fluent API) adds
|
|
111
|
+
> roughly 1-2 ns per method call — effectively zero.
|
|
112
|
+
|
|
113
|
+
### Matching vs `try/catch`
|
|
114
|
+
|
|
115
|
+
| Pattern | ops/s | Latency |
|
|
116
|
+
|---------|------:|--------:|
|
|
117
|
+
| `match(Ok)` | 23,395,940 | 46.9 ns |
|
|
118
|
+
| `match(Err)` | 23,228,941 | 46.1 ns |
|
|
119
|
+
| `try/catch` (no throw) | 25,834,736 | 41.5 ns |
|
|
120
|
+
| `try/catch` (throw) | 1,137,303 | 1154.6 ns |
|
|
121
|
+
|
|
122
|
+
> `match` is within 15% of raw try/catch on the success path.
|
|
123
|
+
> On error paths, `match` is **20× faster** than try/catch with a thrown error.
|
|
124
|
+
|
|
125
|
+
### Collections
|
|
126
|
+
|
|
127
|
+
| Pattern | ops/s | Latency |
|
|
128
|
+
|---------|------:|--------:|
|
|
129
|
+
| `all(5 ok)` | 9,896,702 | 120.3 ns |
|
|
130
|
+
| `all(5, last err)` | 13,444,957 | 91.8 ns |
|
|
131
|
+
| `any(5 ok)` | 15,383,201 | 79.1 ns |
|
|
132
|
+
|
|
133
|
+
### Pattern Matching (`matchErr`)
|
|
134
|
+
|
|
135
|
+
| Pattern | ops/s | Latency |
|
|
136
|
+
|---------|------:|--------:|
|
|
137
|
+
| `matchErr` (hit — creates Error) | 1,005,546 | 1175.9 ns |
|
|
138
|
+
| `matchErr` (miss — instanceof only) | 14,971,304 | 80.0 ns |
|
|
139
|
+
|
|
140
|
+
> The "hit" path creates the matched `TypedError`, which is the same cost as `new Error()`.
|
|
141
|
+
> The "miss" path is just an `instanceof` check — 15M ops/s.
|
|
142
|
+
|
|
143
|
+
### Context
|
|
144
|
+
|
|
145
|
+
| Pattern | ops/s | Latency |
|
|
146
|
+
|---------|------:|--------:|
|
|
147
|
+
| `context()` wrap | 1,198,643 | 1035.6 ns |
|
|
148
|
+
| `context()` with help | 1,162,701 | 1137.3 ns |
|
|
149
|
+
|
|
150
|
+
> Creating structured `Report` objects has the same cost as `new Error` (~1 µs).
|
|
151
|
+
|
|
152
|
+
### Real-World Patterns
|
|
153
|
+
|
|
154
|
+
| Pattern | ops/s | Latency |
|
|
155
|
+
|---------|------:|--------:|
|
|
156
|
+
| parse JSON (ripthrow) | 7,746,842 | 141.6 ns |
|
|
157
|
+
| parse JSON (try/catch) | 8,546,584 | 126.5 ns |
|
|
158
|
+
| deep access (ripthrow) | 18,689,730 | 60.4 ns |
|
|
159
|
+
| deep access (try/catch) | 25,080,105 | 42.4 ns |
|
|
160
|
+
| legacy throw fn (ripthrow) | 18,203,471 | 63.3 ns |
|
|
161
|
+
| legacy throw fn (try/catch) | 25,927,726 | 40.8 ns |
|
|
162
|
+
|
|
163
|
+
> In real-world usage, ripthrow adds 15-20 ns per operation — well within the
|
|
164
|
+
> "zero overhead" claim for all practical purposes. The bottleneck is never
|
|
165
|
+
> the Result type, it's whatever you're doing inside `map`/`andThen`.
|
|
166
|
+
|
|
167
|
+
### Summary
|
|
168
|
+
|
|
169
|
+
- **`Ok`/`Err` = native object literal speed.** Same representation, same performance.
|
|
170
|
+
- **`map`/`andThen` = 1-5 ns overhead** per function call.
|
|
171
|
+
- **`match` = faster than try/catch** when errors are actually thrown.
|
|
172
|
+
- **`safe()` = same speed as manual try/catch** on success paths.
|
|
173
|
+
- **`matchErr` "miss" path = 15M ops/s**, "hit" path = Error construction speed.
|
|
174
|
+
- **Cost center is always `new Error()`**, not ripthrow.
|
|
175
|
+
|
|
176
|
+
> "Zero overhead" is not marketing — it's measured. ripthrow compiles down to
|
|
177
|
+
> object literal checks and function calls with no hidden allocation or
|
|
178
|
+
> prototype magic.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ripthrow",
|
|
3
3
|
"module": "src/index.ts",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.1.1",
|
|
5
5
|
"description": "Zero-overhead, type-safe error handling for TypeScript.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"result",
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
"@types/bun": "latest",
|
|
33
33
|
"@types/node": "^25.6.1",
|
|
34
34
|
"knip": "^6.12.1",
|
|
35
|
+
"tinybench": "^6.0.1",
|
|
35
36
|
"typedoc": "^0.28.19",
|
|
36
37
|
"typedoc-plugin-markdown": "^4.11.0"
|
|
37
38
|
},
|
|
@@ -42,6 +43,7 @@
|
|
|
42
43
|
"knip": "knip",
|
|
43
44
|
"lint": "biome check .",
|
|
44
45
|
"format": "biome format --write .",
|
|
45
|
-
"docs": "typedoc"
|
|
46
|
+
"docs": "typedoc",
|
|
47
|
+
"bench": "bun run benches/result.bench.ts"
|
|
46
48
|
}
|
|
47
49
|
}
|
package/src/operators/context.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { mapErr } from "./map-err";
|
|
|
10
10
|
* @param result The Result to attach context to.
|
|
11
11
|
* @param message The context message.
|
|
12
12
|
* @param help An optional help message.
|
|
13
|
+
* @param meta Optional metadata to attach (merged with any _metadata from the original error).
|
|
13
14
|
* @returns A Result with the same success value or a Report as the error.
|
|
14
15
|
*
|
|
15
16
|
* @category Operators
|
|
@@ -20,6 +21,20 @@ export function context<T, E>(
|
|
|
20
21
|
result: Result<T, E>,
|
|
21
22
|
message: string,
|
|
22
23
|
help?: string,
|
|
24
|
+
meta?: Record<string, unknown>,
|
|
23
25
|
): Result<T, Report> {
|
|
24
|
-
return mapErr(result, (err) =>
|
|
26
|
+
return mapErr(result, (err) => {
|
|
27
|
+
let originalMeta: Record<string, unknown> | undefined;
|
|
28
|
+
if (err && typeof err === "object") {
|
|
29
|
+
originalMeta = (err as { _metadata?: Record<string, unknown> })._metadata;
|
|
30
|
+
}
|
|
31
|
+
const merged = { ...(originalMeta || {}), ...(meta || {}) };
|
|
32
|
+
const keys = Object.keys(merged);
|
|
33
|
+
// biome-ignore lint/nursery/noTernary: it's more readable
|
|
34
|
+
const ctx: Record<string, unknown> | undefined = keys.length > 0 ? merged : undefined;
|
|
35
|
+
return Report.from(err, message, {
|
|
36
|
+
help,
|
|
37
|
+
context: ctx,
|
|
38
|
+
});
|
|
39
|
+
});
|
|
25
40
|
}
|
package/src/pattern.ts
CHANGED
|
@@ -9,6 +9,7 @@ interface TypedError<A extends unknown[], N extends string> extends Error {
|
|
|
9
9
|
readonly help?: string;
|
|
10
10
|
readonly name: N;
|
|
11
11
|
readonly kind: N;
|
|
12
|
+
readonly _metadata?: Record<string, unknown>;
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
interface HandlerEntry {
|
|
@@ -25,6 +26,7 @@ interface ErrDefEntry {
|
|
|
25
26
|
message: (...args: any[]) => string;
|
|
26
27
|
// biome-ignore lint/suspicious/noExplicitAny: required for Parameters<>
|
|
27
28
|
help?: (...args: any[]) => string;
|
|
29
|
+
_metadata?: Record<string, unknown>;
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
type ErrDefMap = Record<string, ErrDefEntry>;
|
|
@@ -44,7 +46,7 @@ type ErrorFactories<T extends ErrDefMap> = {
|
|
|
44
46
|
export function createErrors<T extends ErrDefMap>(defs: T): ErrorFactories<T> {
|
|
45
47
|
const result: Record<string, ErrFactory<unknown[], string>> = {};
|
|
46
48
|
for (const [name, def] of Object.entries(defs)) {
|
|
47
|
-
result[name] = createError(name, def.message, def.help);
|
|
49
|
+
result[name] = createError(name, def.message, def.help, def._metadata);
|
|
48
50
|
}
|
|
49
51
|
return result as unknown as ErrorFactories<T>;
|
|
50
52
|
}
|
|
@@ -53,17 +55,20 @@ export function createError<A extends unknown[], N extends string>(
|
|
|
53
55
|
name: N,
|
|
54
56
|
message: (...args: A) => string,
|
|
55
57
|
help?: (...args: A) => string,
|
|
58
|
+
_metadata?: Record<string, unknown>,
|
|
56
59
|
): ErrFactory<A, N> {
|
|
57
60
|
class _Error extends Error {
|
|
58
61
|
override readonly name: N;
|
|
59
62
|
readonly args: A;
|
|
60
63
|
readonly help: string | undefined;
|
|
61
64
|
readonly kind: N = name;
|
|
65
|
+
readonly _metadata: Record<string, unknown> | undefined;
|
|
62
66
|
|
|
63
67
|
constructor(...args: A) {
|
|
64
68
|
super(message(...args));
|
|
65
69
|
this.name = name;
|
|
66
70
|
this.args = args;
|
|
71
|
+
this._metadata = _metadata;
|
|
67
72
|
if (help) {
|
|
68
73
|
this.help = help(...args);
|
|
69
74
|
}
|
|
@@ -14,11 +14,36 @@ import { tapErr } from "./operators/tap-err";
|
|
|
14
14
|
import type { Report } from "./report";
|
|
15
15
|
import type { AsyncResult, Result } from "./types/result";
|
|
16
16
|
|
|
17
|
+
type Op = (
|
|
18
|
+
r: Result<unknown, unknown>,
|
|
19
|
+
) => Result<unknown, unknown> | Promise<Result<unknown, unknown>>;
|
|
20
|
+
|
|
17
21
|
export class AsyncResultBuilder<T, E> {
|
|
18
22
|
private readonly _promise: AsyncResult<T, E>;
|
|
23
|
+
private readonly _ops: Op[];
|
|
24
|
+
private _executed: AsyncResult<T, E> | null = null;
|
|
19
25
|
|
|
20
|
-
constructor(promise: AsyncResult<T, E
|
|
26
|
+
constructor(promise: AsyncResult<T, E>, ops?: Op[]) {
|
|
21
27
|
this._promise = promise;
|
|
28
|
+
this._ops = ops ?? [];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
private execute(): AsyncResult<T, E> {
|
|
32
|
+
if (this._executed) {
|
|
33
|
+
return this._executed;
|
|
34
|
+
}
|
|
35
|
+
if (this._ops.length === 0) {
|
|
36
|
+
this._executed = this._promise;
|
|
37
|
+
} else {
|
|
38
|
+
this._executed = this._promise.then(async (r) => {
|
|
39
|
+
let current: Result<unknown, unknown> = r;
|
|
40
|
+
for (const op of this._ops) {
|
|
41
|
+
current = await op(current);
|
|
42
|
+
}
|
|
43
|
+
return current as Result<T, E>;
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
return this._executed;
|
|
22
47
|
}
|
|
23
48
|
|
|
24
49
|
static ok<U = void, F = unknown>(value?: U): AsyncResultBuilder<U, F> {
|
|
@@ -70,69 +95,94 @@ export class AsyncResultBuilder<T, E> {
|
|
|
70
95
|
}
|
|
71
96
|
|
|
72
97
|
get result(): AsyncResult<T, E> {
|
|
73
|
-
return this.
|
|
98
|
+
return this.execute();
|
|
74
99
|
}
|
|
75
100
|
|
|
76
101
|
get isOk(): Promise<boolean> {
|
|
77
|
-
return this.
|
|
102
|
+
return this.execute().then(isOk);
|
|
78
103
|
}
|
|
79
104
|
|
|
80
105
|
get isErr(): Promise<boolean> {
|
|
81
|
-
return this.
|
|
106
|
+
return this.execute().then(isErr);
|
|
82
107
|
}
|
|
83
108
|
|
|
84
109
|
map<R>(fn: (value: T) => R): AsyncResultBuilder<R, E> {
|
|
85
|
-
|
|
110
|
+
const op: Op = (r: Result<unknown, unknown>) => map(r as Result<T, E>, fn);
|
|
111
|
+
return new AsyncResultBuilder<R, E>(this._promise as unknown as AsyncResult<R, E>, [
|
|
112
|
+
...this._ops,
|
|
113
|
+
op,
|
|
114
|
+
]);
|
|
86
115
|
}
|
|
87
116
|
|
|
88
117
|
mapErr<F>(fn: (error: E) => F): AsyncResultBuilder<T, F> {
|
|
89
|
-
|
|
118
|
+
const op: Op = (r: Result<unknown, unknown>) => mapErr(r as Result<T, E>, fn);
|
|
119
|
+
return new AsyncResultBuilder<T, F>(this._promise as unknown as AsyncResult<T, F>, [
|
|
120
|
+
...this._ops,
|
|
121
|
+
op,
|
|
122
|
+
]);
|
|
90
123
|
}
|
|
91
124
|
|
|
92
125
|
andThen<R>(fn: (value: T) => Result<R, E> | AsyncResult<R, E>): AsyncResultBuilder<R, E> {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
126
|
+
const op: Op = (r: Result<unknown, unknown>) => {
|
|
127
|
+
const res = r as Result<T, E>;
|
|
128
|
+
if (!res.ok) {
|
|
129
|
+
return res;
|
|
130
|
+
}
|
|
131
|
+
return fn(res.value);
|
|
132
|
+
};
|
|
133
|
+
return new AsyncResultBuilder<R, E>(this._promise as unknown as AsyncResult<R, E>, [
|
|
134
|
+
...this._ops,
|
|
135
|
+
op,
|
|
136
|
+
]);
|
|
101
137
|
}
|
|
102
138
|
|
|
103
139
|
orElse<F>(fn: (error: E) => Result<T, F> | AsyncResult<T, F>): AsyncResultBuilder<T, F> {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
140
|
+
const op: Op = (r: Result<unknown, unknown>) => {
|
|
141
|
+
const res = r as Result<T, E>;
|
|
142
|
+
if (res.ok) {
|
|
143
|
+
return res;
|
|
144
|
+
}
|
|
145
|
+
return fn(res.error);
|
|
146
|
+
};
|
|
147
|
+
return new AsyncResultBuilder<T, F>(this._promise as unknown as AsyncResult<T, F>, [
|
|
148
|
+
...this._ops,
|
|
149
|
+
op,
|
|
150
|
+
]);
|
|
112
151
|
}
|
|
113
152
|
|
|
114
153
|
tap(fn: (value: T) => void): AsyncResultBuilder<T, E> {
|
|
115
|
-
|
|
154
|
+
const op: Op = (r: Result<unknown, unknown>) => tap(r as Result<T, E>, fn);
|
|
155
|
+
return new AsyncResultBuilder<T, E>(this._promise, [...this._ops, op]);
|
|
116
156
|
}
|
|
117
157
|
|
|
118
158
|
tapErr(fn: (error: E) => void): AsyncResultBuilder<T, E> {
|
|
119
|
-
|
|
159
|
+
const op: Op = (r: Result<unknown, unknown>) => tapErr(r as Result<T, E>, fn);
|
|
160
|
+
return new AsyncResultBuilder<T, E>(this._promise, [...this._ops, op]);
|
|
120
161
|
}
|
|
121
162
|
|
|
122
|
-
context(
|
|
123
|
-
|
|
163
|
+
context(
|
|
164
|
+
message: string,
|
|
165
|
+
help?: string,
|
|
166
|
+
meta?: Record<string, unknown>,
|
|
167
|
+
): AsyncResultBuilder<T, Report> {
|
|
168
|
+
const op: Op = (r: Result<unknown, unknown>) =>
|
|
169
|
+
contextOp(r as Result<T, E>, message, help, meta);
|
|
170
|
+
return new AsyncResultBuilder<T, Report>(this._promise as unknown as AsyncResult<T, Report>, [
|
|
171
|
+
...this._ops,
|
|
172
|
+
op,
|
|
173
|
+
]);
|
|
124
174
|
}
|
|
125
175
|
|
|
126
176
|
match<R>(handlers: { ok: (value: T) => R; err: (error: E) => R }): Promise<R> {
|
|
127
|
-
return this.
|
|
177
|
+
return this.execute().then((r) => match(r, handlers));
|
|
128
178
|
}
|
|
129
179
|
|
|
130
180
|
unwrapOr(defaultValue: T): Promise<T> {
|
|
131
|
-
return this.
|
|
181
|
+
return this.execute().then((r) => unwrapOr(r, defaultValue));
|
|
132
182
|
}
|
|
133
183
|
|
|
134
184
|
unwrap(): Promise<T> {
|
|
135
|
-
return this.
|
|
185
|
+
return this.execute().then((r) => unwrap(r));
|
|
136
186
|
}
|
|
137
187
|
}
|
|
138
188
|
|
package/src/result-builder.ts
CHANGED
|
@@ -175,8 +175,12 @@ export class ResultBuilder<T, E> {
|
|
|
175
175
|
/**
|
|
176
176
|
* Attaches context to the error if it exists.
|
|
177
177
|
*/
|
|
178
|
-
context(
|
|
179
|
-
|
|
178
|
+
context(
|
|
179
|
+
message: string,
|
|
180
|
+
help?: string,
|
|
181
|
+
meta?: Record<string, unknown>,
|
|
182
|
+
): ResultBuilder<T, Report> {
|
|
183
|
+
return new ResultBuilder(contextOp(this._result, message, help, meta));
|
|
180
184
|
}
|
|
181
185
|
}
|
|
182
186
|
|
package/src/utils/index.ts
CHANGED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extracts the error `kind` from any ripthrow error.
|
|
3
|
+
* Handles TypedError directly and Report (traverses cause).
|
|
4
|
+
*
|
|
5
|
+
* @param err The error to extract the kind from.
|
|
6
|
+
* @returns The error kind string, or undefined if not found.
|
|
7
|
+
*
|
|
8
|
+
* @category Utilities
|
|
9
|
+
* @example
|
|
10
|
+
* const kind = kindOf(err);
|
|
11
|
+
* if (kind === "userNotFound") { ... }
|
|
12
|
+
*/
|
|
13
|
+
export function kindOf(err: unknown): string | undefined {
|
|
14
|
+
if (err && typeof err === "object" && "kind" in err) {
|
|
15
|
+
return (err as { kind: string }).kind;
|
|
16
|
+
}
|
|
17
|
+
if (err instanceof Error && err.cause && typeof err.cause === "object" && "kind" in err.cause) {
|
|
18
|
+
return (err.cause as { kind: string }).kind;
|
|
19
|
+
}
|
|
20
|
+
// biome-ignore lint/complexity/noUselessUndefined: required by noImplicitReturns
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pipes a value through a series of transform functions.
|
|
3
|
+
* Accepts both sync and Promise values — awaits the input if needed.
|
|
4
|
+
*
|
|
5
|
+
* @category Utilities
|
|
6
|
+
* @example
|
|
7
|
+
* const result = await pipe(
|
|
8
|
+
* safe(() => JSON.parse(input)),
|
|
9
|
+
* (r) => map(r, (data: any) => data.a),
|
|
10
|
+
* (r) => unwrapOr(r, 0),
|
|
11
|
+
* );
|
|
12
|
+
*/
|
|
13
|
+
// biome-ignore lint/suspicious/noExplicitAny: variadic pipe needs flexibility
|
|
14
|
+
export async function pipe<T, Fns extends Array<(arg: any) => any>>(
|
|
15
|
+
value: T | Promise<T>,
|
|
16
|
+
...fns: Fns
|
|
17
|
+
// biome-ignore lint/suspicious/noExplicitAny: conditional type inference
|
|
18
|
+
): Promise<Fns extends [...any[], infer Last] ? (Last extends (arg: any) => infer R ? R : T) : T> {
|
|
19
|
+
let acc: unknown = await value;
|
|
20
|
+
for (const fn of fns) {
|
|
21
|
+
// biome-ignore lint/suspicious/noExplicitAny: variadic pipe
|
|
22
|
+
acc = (fn as any)(acc);
|
|
23
|
+
}
|
|
24
|
+
return acc as never;
|
|
25
|
+
}
|