-
Notifications
You must be signed in to change notification settings - Fork 0
/
result.ts
153 lines (138 loc) · 4.22 KB
/
result.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
import { Maybe } from './maybe.js';
/**
* An abstract box representing success or failure.
* Provides various ways to perform operations on either of those state,
* without explicitly checking for it.
*/
export class Result<T, E> {
#state;
private constructor(
state: { isOk: true; value: T } | { isOk: false; err: E },
) {
this.#state = state;
}
/**
* Create a box representing failure
*/
static Err<const E>(err: E) {
return new Result<never, E>({ isOk: false, err });
}
/**
* Create a box representing success
*/
static Ok<T>(value: T) {
return new Result<T, never>({ isOk: true, value });
}
/**
* Combine record of boxes and return either record with their unboxed values
* if all boxes represent a success or the first box representing failure
* otherwise
*/
static all<T extends Readonly<Record<string, Result<unknown, unknown>>>>(
xs: T,
): Result<
{ [K in keyof T]: T[K] extends Result<infer U, unknown> ? U : never },
{
[K in keyof T]: T[K] extends Result<unknown, infer F> ? F : never;
}[keyof T]
> {
const result: Record<string, unknown> = {};
for (const [key, x] of Object.entries(xs)) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if (!x.#state.isOk) return x as any;
result[key] = x.#state.value;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return Result.Ok(result as any);
}
/**
* Unpack the box. Requires explicit handling of both success and failure.
* This is used to implement most of the operations
*/
match<U, F = U>(arms: { Ok: (value: T) => U; Err: (value: E) => F }) {
return this.#state.isOk
? arms.Ok(this.#state.value)
: arms.Err(this.#state.err);
}
/**
* Apply an arbitrary transformation to a success value inside the box and
* return a new box containing the result of that transformation.
* `f` accepts an existing value. Returned value will be put inside the new
* box representing success. Not called if a box represents failure
*/
mapOk<U>(f: (value: T) => U) {
return this.match<Result<U, E>>({
Err: Result.Err,
Ok: value => Result.Ok(f(value)),
});
}
/**
* Apply an arbitrary transformation to a failure value inside the box and
* return a new box containing the result of that transformation.
* `f` accepts an existing value. Returned value will be put inside the new
* box representing failure. Not called if a box represents success
*/
mapErr<F>(f: (err: E) => F) {
return this.match<Result<T, F>>({
Ok: Result.Ok,
Err: err => Result.Err(f(err)),
});
}
/**
* Apply an arbitrary transformation to a success value inside the box and
* return a new box with the result of that transformation.
* As opposed to `mapOk`, `f` is required to return a new box, not just a
* value.
* This can be useful if you want to turn `Result.Ok` into `Result.Err`
* depending on it's value
*/
flatMapOk<U, F>(f: (value: T) => Result<U, F>) {
return this.match<Result<U, E | F>>({
Err: Result.Err,
Ok: value => f(value),
});
}
/**
* Apply an arbitrary transformation to a failure value inside the box and
* return a new box with the result of that transformation.
* As opposed to `mapErr`, `f` is required to return a new box, not just a
* value.
* This can be useful if you want to turn `Result.Err` into `Result.Ok`
* depending on it's value
*/
flatMapErr<U, F>(f: (err: E) => Result<U, F>) {
return this.match<Result<T | U, F>>({
Err: err => f(err),
Ok: Result.Ok,
});
}
/**
* Turn this `Result` into a `Maybe` of a success value.
*/
getOk() {
return this.match<Maybe<T>>({
Ok: Maybe.Some,
Err: () => Maybe.None,
});
}
/**
* Turn this `Result` into a `Maybe` of a failure value.
*/
getErr() {
return this.match<Maybe<E>>({
Ok: () => Maybe.None,
Err: Maybe.Some,
});
}
/**
* Return a boxed success value or throw an error if box is in failure state
*/
assertOk(message = 'Assertion failed') {
return this.match({
Ok: value => value,
Err: cause => {
throw new Error(message, { cause });
},
});
}
}