@iahuang/result-ts 1.2.0 → 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/.claude/settings.local.json +4 -1
- package/README.md +176 -109
- package/dist/index.d.ts +72 -111
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +176 -180
- package/dist/index.js.map +1 -1
- package/index.test.ts +782 -0
- package/index.ts +134 -179
- package/package.json +6 -3
package/index.test.ts
ADDED
|
@@ -0,0 +1,782 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
ok,
|
|
4
|
+
err,
|
|
5
|
+
resultType,
|
|
6
|
+
resultAnd,
|
|
7
|
+
resultAndThen,
|
|
8
|
+
resultErr,
|
|
9
|
+
resultExpect,
|
|
10
|
+
resultExpectErr,
|
|
11
|
+
resultFlatten,
|
|
12
|
+
resultInspect,
|
|
13
|
+
resultInspectErr,
|
|
14
|
+
resultIsErr,
|
|
15
|
+
resultIsErrAnd,
|
|
16
|
+
resultIsOk,
|
|
17
|
+
resultIsOkAnd,
|
|
18
|
+
resultMap,
|
|
19
|
+
resultMapErr,
|
|
20
|
+
resultMapOr,
|
|
21
|
+
resultMapOrElse,
|
|
22
|
+
resultOk,
|
|
23
|
+
resultOr,
|
|
24
|
+
resultOrElse,
|
|
25
|
+
resultUnwrap,
|
|
26
|
+
resultUnwrapErr,
|
|
27
|
+
resultUnwrapOr,
|
|
28
|
+
resultUnwrapOrElse,
|
|
29
|
+
resultFromThrowingFunction,
|
|
30
|
+
resultFromThrowingAsyncFunction,
|
|
31
|
+
resultFromThrowingPromise,
|
|
32
|
+
chain,
|
|
33
|
+
asyncChain,
|
|
34
|
+
match,
|
|
35
|
+
type Result,
|
|
36
|
+
type Err,
|
|
37
|
+
} from "./index";
|
|
38
|
+
|
|
39
|
+
// --- Test helpers ---
|
|
40
|
+
|
|
41
|
+
type TestErrors = {
|
|
42
|
+
not_found: string;
|
|
43
|
+
invalid: number;
|
|
44
|
+
empty: undefined;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const r = resultType<string, TestErrors>();
|
|
48
|
+
|
|
49
|
+
function mkOk(): Result<string, TestErrors> {
|
|
50
|
+
return r.ok("hello");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function mkErr(): Result<string, TestErrors> {
|
|
54
|
+
return r.err("not_found", "/path");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// --- Result shape tests ---
|
|
58
|
+
|
|
59
|
+
describe("Result shape", () => {
|
|
60
|
+
it("ok() produces { ok: true, value }", () => {
|
|
61
|
+
const result = ok(42);
|
|
62
|
+
expect(result).toEqual({ ok: true, value: 42 });
|
|
63
|
+
expect(result.ok).toBe(true);
|
|
64
|
+
expect(result.value).toBe(42);
|
|
65
|
+
expect(result.error).toBeUndefined();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("err() produces { ok: false, error: { type, detail } }", () => {
|
|
69
|
+
const result = err("not_found", "/missing");
|
|
70
|
+
expect(result).toEqual({
|
|
71
|
+
ok: false,
|
|
72
|
+
error: { type: "not_found", detail: "/missing" },
|
|
73
|
+
});
|
|
74
|
+
expect(result.ok).toBe(false);
|
|
75
|
+
expect(result.error).toEqual({ type: "not_found", detail: "/missing" });
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("ok result has value and undefined error", () => {
|
|
79
|
+
const result = mkOk();
|
|
80
|
+
if (result.ok) {
|
|
81
|
+
expect(result.value).toBe("hello");
|
|
82
|
+
expect(result.error).toBeUndefined();
|
|
83
|
+
} else {
|
|
84
|
+
throw new Error("expected ok");
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("err result has error and undefined value", () => {
|
|
89
|
+
const result = mkErr();
|
|
90
|
+
if (!result.ok) {
|
|
91
|
+
expect(result.error.type).toBe("not_found");
|
|
92
|
+
expect(result.error.detail).toBe("/path");
|
|
93
|
+
expect(result.value).toBeUndefined();
|
|
94
|
+
} else {
|
|
95
|
+
throw new Error("expected err");
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// --- resultType constructors ---
|
|
101
|
+
|
|
102
|
+
describe("resultType constructors", () => {
|
|
103
|
+
it("ok constructor creates ok results", () => {
|
|
104
|
+
const result = r.ok("test");
|
|
105
|
+
expect(result.ok).toBe(true);
|
|
106
|
+
if (result.ok) expect(result.value).toBe("test");
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("err constructor creates err results with detail", () => {
|
|
110
|
+
const result = r.err("not_found", "/file");
|
|
111
|
+
expect(result.ok).toBe(false);
|
|
112
|
+
if (!result.ok) {
|
|
113
|
+
expect(result.error.type).toBe("not_found");
|
|
114
|
+
expect(result.error.detail).toBe("/file");
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("err constructor creates err results without detail (undefined)", () => {
|
|
119
|
+
const result = r.err("empty");
|
|
120
|
+
expect(result.ok).toBe(false);
|
|
121
|
+
if (!result.ok) {
|
|
122
|
+
expect(result.error.type).toBe("empty");
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// --- Standalone functions ---
|
|
128
|
+
|
|
129
|
+
describe("resultAnd", () => {
|
|
130
|
+
it("returns other when ok", () => {
|
|
131
|
+
const result = resultAnd(mkOk(), r.ok("world"));
|
|
132
|
+
expect(result.ok).toBe(true);
|
|
133
|
+
if (result.ok) expect(result.value).toBe("world");
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("returns err when err", () => {
|
|
137
|
+
const result = resultAnd(mkErr(), r.ok("world"));
|
|
138
|
+
expect(result.ok).toBe(false);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
describe("resultAndThen", () => {
|
|
143
|
+
it("calls fn when ok", () => {
|
|
144
|
+
const result = resultAndThen(mkOk(), (v) => r.ok(v + " world"));
|
|
145
|
+
expect(result.ok).toBe(true);
|
|
146
|
+
if (result.ok) expect(result.value).toBe("hello world");
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("returns err when err", () => {
|
|
150
|
+
const result = resultAndThen(mkErr(), (v) => r.ok(v + " world"));
|
|
151
|
+
expect(result.ok).toBe(false);
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
describe("resultErr", () => {
|
|
156
|
+
it("returns error info when err", () => {
|
|
157
|
+
const e = resultErr(mkErr());
|
|
158
|
+
expect(e).toEqual({ type: "not_found", detail: "/path" });
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("returns null when ok", () => {
|
|
162
|
+
expect(resultErr(mkOk())).toBeNull();
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
describe("resultExpect", () => {
|
|
167
|
+
it("returns value when ok", () => {
|
|
168
|
+
expect(resultExpect(mkOk(), "should not throw")).toBe("hello");
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("throws with message when err", () => {
|
|
172
|
+
expect(() => resultExpect(mkErr(), "custom msg")).toThrow("custom msg");
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
describe("resultExpectErr", () => {
|
|
177
|
+
it("returns error info when err", () => {
|
|
178
|
+
const e = resultExpectErr(mkErr(), "should not throw");
|
|
179
|
+
expect(e).toEqual({ type: "not_found", detail: "/path" });
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("throws with message when ok", () => {
|
|
183
|
+
expect(() => resultExpectErr(mkOk(), "custom msg")).toThrow("custom msg");
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
describe("resultFlatten", () => {
|
|
188
|
+
it("flattens nested ok", () => {
|
|
189
|
+
const inner = r.ok("inner");
|
|
190
|
+
const outer: Result<Result<string, TestErrors>, TestErrors> = ok(inner);
|
|
191
|
+
const flat = resultFlatten(outer);
|
|
192
|
+
expect(flat.ok).toBe(true);
|
|
193
|
+
if (flat.ok) expect(flat.value).toBe("inner");
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("flattens outer err", () => {
|
|
197
|
+
const outer: Result<Result<string, TestErrors>, TestErrors> = mkErr() as any;
|
|
198
|
+
const flat = resultFlatten(outer);
|
|
199
|
+
expect(flat.ok).toBe(false);
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
describe("resultInspect", () => {
|
|
204
|
+
it("calls fn with value when ok", () => {
|
|
205
|
+
let inspected: string | undefined;
|
|
206
|
+
const result = resultInspect(mkOk(), (v) => { inspected = v; });
|
|
207
|
+
expect(inspected).toBe("hello");
|
|
208
|
+
expect(result.ok).toBe(true);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it("does not call fn when err", () => {
|
|
212
|
+
let called = false;
|
|
213
|
+
resultInspect(mkErr(), () => { called = true; });
|
|
214
|
+
expect(called).toBe(false);
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
describe("resultInspectErr", () => {
|
|
219
|
+
it("calls fn with error info when err", () => {
|
|
220
|
+
let inspected: Err<TestErrors> | undefined;
|
|
221
|
+
resultInspectErr(mkErr(), (e) => { inspected = e; });
|
|
222
|
+
expect(inspected).toEqual({ type: "not_found", detail: "/path" });
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it("does not call fn when ok", () => {
|
|
226
|
+
let called = false;
|
|
227
|
+
resultInspectErr(mkOk(), () => { called = true; });
|
|
228
|
+
expect(called).toBe(false);
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
describe("resultIsErr / resultIsOk", () => {
|
|
233
|
+
it("resultIsErr returns true for err", () => {
|
|
234
|
+
expect(resultIsErr(mkErr())).toBe(true);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it("resultIsErr returns false for ok", () => {
|
|
238
|
+
expect(resultIsErr(mkOk())).toBe(false);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it("resultIsOk returns true for ok", () => {
|
|
242
|
+
expect(resultIsOk(mkOk())).toBe(true);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it("resultIsOk returns false for err", () => {
|
|
246
|
+
expect(resultIsOk(mkErr())).toBe(false);
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
describe("resultIsErrAnd / resultIsOkAnd", () => {
|
|
251
|
+
it("resultIsErrAnd returns true when predicate matches", () => {
|
|
252
|
+
expect(resultIsErrAnd(mkErr(), (e) => e.type === "not_found")).toBe(true);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it("resultIsErrAnd returns false when predicate fails", () => {
|
|
256
|
+
expect(resultIsErrAnd(mkErr(), (e) => e.type === "invalid")).toBe(false);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it("resultIsErrAnd returns false for ok", () => {
|
|
260
|
+
expect(resultIsErrAnd(mkOk(), () => true)).toBe(false);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it("resultIsOkAnd returns true when predicate matches", () => {
|
|
264
|
+
expect(resultIsOkAnd(mkOk(), (v) => v === "hello")).toBe(true);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it("resultIsOkAnd returns false when predicate fails", () => {
|
|
268
|
+
expect(resultIsOkAnd(mkOk(), (v) => v === "nope")).toBe(false);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it("resultIsOkAnd returns false for err", () => {
|
|
272
|
+
expect(resultIsOkAnd(mkErr(), () => true)).toBe(false);
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
describe("resultMap", () => {
|
|
277
|
+
it("maps value when ok", () => {
|
|
278
|
+
const result = resultMap(mkOk(), (v) => v.length);
|
|
279
|
+
expect(result.ok).toBe(true);
|
|
280
|
+
if (result.ok) expect(result.value).toBe(5);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it("passes through err", () => {
|
|
284
|
+
const result = resultMap(mkErr(), (v) => v.length);
|
|
285
|
+
expect(result.ok).toBe(false);
|
|
286
|
+
if (!result.ok) expect(result.error.type).toBe("not_found");
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
describe("resultMapErr", () => {
|
|
291
|
+
it("maps error when err", () => {
|
|
292
|
+
const result = resultMapErr(mkErr(), (e) => ({
|
|
293
|
+
type: "general" as const,
|
|
294
|
+
detail: `${e.type}: ${e.detail}`,
|
|
295
|
+
}));
|
|
296
|
+
expect(result.ok).toBe(false);
|
|
297
|
+
if (!result.ok) {
|
|
298
|
+
expect(result.error.type).toBe("general");
|
|
299
|
+
expect(result.error.detail).toBe("not_found: /path");
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it("passes through ok", () => {
|
|
304
|
+
const result = resultMapErr(mkOk(), (e) => ({
|
|
305
|
+
type: "general" as const,
|
|
306
|
+
detail: String(e.detail),
|
|
307
|
+
}));
|
|
308
|
+
expect(result.ok).toBe(true);
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
describe("resultMapOr", () => {
|
|
313
|
+
it("applies fn when ok", () => {
|
|
314
|
+
expect(resultMapOr(mkOk(), 0, (v) => v.length)).toBe(5);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it("returns default when err", () => {
|
|
318
|
+
expect(resultMapOr(mkErr(), 0, (v) => v.length)).toBe(0);
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
describe("resultMapOrElse", () => {
|
|
323
|
+
it("applies fn when ok", () => {
|
|
324
|
+
expect(resultMapOrElse(mkOk(), () => 0, (v) => v.length)).toBe(5);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it("applies default fn when err", () => {
|
|
328
|
+
expect(
|
|
329
|
+
resultMapOrElse(mkErr(), (e) => (e.type === "not_found" ? -1 : -2), (v) => v.length)
|
|
330
|
+
).toBe(-1);
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
describe("resultOk", () => {
|
|
335
|
+
it("returns value when ok", () => {
|
|
336
|
+
expect(resultOk(mkOk())).toBe("hello");
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it("returns null when err", () => {
|
|
340
|
+
expect(resultOk(mkErr())).toBeNull();
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
describe("resultOr", () => {
|
|
345
|
+
it("returns first when ok", () => {
|
|
346
|
+
const result = resultOr(mkOk(), r.ok("other"));
|
|
347
|
+
if (result.ok) expect(result.value).toBe("hello");
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it("returns other when err", () => {
|
|
351
|
+
const result = resultOr(mkErr(), r.ok("other"));
|
|
352
|
+
if (result.ok) expect(result.value).toBe("other");
|
|
353
|
+
});
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
describe("resultOrElse", () => {
|
|
357
|
+
it("returns first when ok", () => {
|
|
358
|
+
const result = resultOrElse(mkOk(), () => r.ok("fallback"));
|
|
359
|
+
if (result.ok) expect(result.value).toBe("hello");
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
it("calls fn when err", () => {
|
|
363
|
+
const result = resultOrElse(mkErr(), (e) => r.ok(`recovered: ${e.type}`));
|
|
364
|
+
if (result.ok) expect(result.value).toBe("recovered: not_found");
|
|
365
|
+
});
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
describe("resultUnwrap", () => {
|
|
369
|
+
it("returns value when ok", () => {
|
|
370
|
+
expect(resultUnwrap(mkOk())).toBe("hello");
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it("throws error info when err", () => {
|
|
374
|
+
try {
|
|
375
|
+
resultUnwrap(mkErr());
|
|
376
|
+
throw new Error("should have thrown");
|
|
377
|
+
} catch (e) {
|
|
378
|
+
expect(e).toEqual({ type: "not_found", detail: "/path" });
|
|
379
|
+
}
|
|
380
|
+
});
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
describe("resultUnwrapErr", () => {
|
|
384
|
+
it("returns error info when err", () => {
|
|
385
|
+
expect(resultUnwrapErr(mkErr())).toEqual({ type: "not_found", detail: "/path" });
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
it("throws when ok", () => {
|
|
389
|
+
expect(() => resultUnwrapErr(mkOk())).toThrow("Result is ok; expected an error");
|
|
390
|
+
});
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
describe("resultUnwrapOr", () => {
|
|
394
|
+
it("returns value when ok", () => {
|
|
395
|
+
expect(resultUnwrapOr(mkOk(), "default")).toBe("hello");
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
it("returns default when err", () => {
|
|
399
|
+
expect(resultUnwrapOr(mkErr(), "default")).toBe("default");
|
|
400
|
+
});
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
describe("resultUnwrapOrElse", () => {
|
|
404
|
+
it("returns value when ok", () => {
|
|
405
|
+
expect(resultUnwrapOrElse(mkOk(), () => "default")).toBe("hello");
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
it("calls fn when err", () => {
|
|
409
|
+
expect(resultUnwrapOrElse(mkErr(), (e) => `err: ${e.type}`)).toBe("err: not_found");
|
|
410
|
+
});
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
// --- tryCatchResult ---
|
|
414
|
+
|
|
415
|
+
describe("tryCatchResult", () => {
|
|
416
|
+
it("returns ok on success", () => {
|
|
417
|
+
const result = resultFromThrowingFunction(
|
|
418
|
+
() => JSON.parse('{"a":1}'),
|
|
419
|
+
(e) => err("parse_error", String(e))
|
|
420
|
+
);
|
|
421
|
+
expect(result.ok).toBe(true);
|
|
422
|
+
if (result.ok) expect(result.value).toEqual({ a: 1 });
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
it("returns err on throw", () => {
|
|
426
|
+
const result = resultFromThrowingFunction(
|
|
427
|
+
() => JSON.parse("invalid"),
|
|
428
|
+
(e) => err("parse_error", String(e))
|
|
429
|
+
);
|
|
430
|
+
expect(result.ok).toBe(false);
|
|
431
|
+
if (!result.ok) {
|
|
432
|
+
expect(result.error.type).toBe("parse_error");
|
|
433
|
+
}
|
|
434
|
+
});
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
// --- match ---
|
|
438
|
+
|
|
439
|
+
describe("match", () => {
|
|
440
|
+
it("calls ok handler when ok", () => {
|
|
441
|
+
const msg = match(mkOk(), {
|
|
442
|
+
ok: (v) => `value: ${v}`,
|
|
443
|
+
err: (e) => `error: ${e.type}`,
|
|
444
|
+
});
|
|
445
|
+
expect(msg).toBe("value: hello");
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
it("calls err handler when err", () => {
|
|
449
|
+
const msg = match(mkErr(), {
|
|
450
|
+
ok: (v) => `value: ${v}`,
|
|
451
|
+
err: (e) => `error: ${e.type}`,
|
|
452
|
+
});
|
|
453
|
+
expect(msg).toBe("error: not_found");
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
it("err handler receives error info with type and detail", () => {
|
|
457
|
+
match(mkErr(), {
|
|
458
|
+
ok: () => {},
|
|
459
|
+
err: (e) => {
|
|
460
|
+
expect(e.type).toBe("not_found");
|
|
461
|
+
expect(e.detail).toBe("/path");
|
|
462
|
+
},
|
|
463
|
+
});
|
|
464
|
+
});
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
// --- Chain API ---
|
|
468
|
+
|
|
469
|
+
describe("chain", () => {
|
|
470
|
+
it("map transforms ok value", () => {
|
|
471
|
+
const result = chain(mkOk()).map((v) => v.length).unwrap();
|
|
472
|
+
expect(result).toBe(5);
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
it("map passes through err", () => {
|
|
476
|
+
const result = chain(mkErr()).map((v) => v.length).unwrapOr(0);
|
|
477
|
+
expect(result).toBe(0);
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
it("andThen chains ok results", () => {
|
|
481
|
+
const result = chain(mkOk())
|
|
482
|
+
.andThen((v) => r.ok(v + " world"))
|
|
483
|
+
.unwrap();
|
|
484
|
+
expect(result).toBe("hello world");
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
it("andThen short-circuits on err", () => {
|
|
488
|
+
let called = false;
|
|
489
|
+
chain(mkErr()).andThen(() => {
|
|
490
|
+
called = true;
|
|
491
|
+
return r.ok("nope");
|
|
492
|
+
});
|
|
493
|
+
expect(called).toBe(false);
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
it("mapErr transforms error", () => {
|
|
497
|
+
const c = chain(mkErr()).mapErr((e) => ({
|
|
498
|
+
type: "wrapped" as const,
|
|
499
|
+
detail: e.type as string,
|
|
500
|
+
}));
|
|
501
|
+
const e = c.err();
|
|
502
|
+
expect(e).toEqual({ type: "wrapped", detail: "not_found" });
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
it("or returns first when ok", () => {
|
|
506
|
+
const result = chain(mkOk()).or(r.ok("other")).unwrap();
|
|
507
|
+
expect(result).toBe("hello");
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
it("or returns other when err", () => {
|
|
511
|
+
const result = chain(mkErr()).or(r.ok("other")).unwrap();
|
|
512
|
+
expect(result).toBe("other");
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
it("orElse calls fn when err", () => {
|
|
516
|
+
const result = chain(mkErr())
|
|
517
|
+
.orElse((e) => r.ok(`recovered: ${e.type}`))
|
|
518
|
+
.unwrap();
|
|
519
|
+
expect(result).toBe("recovered: not_found");
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
it("unwrap throws error info for err", () => {
|
|
523
|
+
try {
|
|
524
|
+
chain(mkErr()).unwrap();
|
|
525
|
+
throw new Error("should have thrown");
|
|
526
|
+
} catch (e) {
|
|
527
|
+
expect(e).toEqual({ type: "not_found", detail: "/path" });
|
|
528
|
+
}
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
it("unwrapOr returns default for err", () => {
|
|
532
|
+
expect(chain(mkErr()).unwrapOr("default")).toBe("default");
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
it("unwrapOrElse calls fn for err", () => {
|
|
536
|
+
expect(chain(mkErr()).unwrapOrElse((e) => `err: ${e.type}`)).toBe("err: not_found");
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
it("expect throws custom message for err", () => {
|
|
540
|
+
expect(() => chain(mkErr()).expect("custom")).toThrow("custom");
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
it("expectErr returns error info for err", () => {
|
|
544
|
+
expect(chain(mkErr()).expectErr("msg")).toEqual({ type: "not_found", detail: "/path" });
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
it("unwrapErr returns error info for err", () => {
|
|
548
|
+
expect(chain(mkErr()).unwrapErr()).toEqual({ type: "not_found", detail: "/path" });
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
it("inspect calls fn for ok", () => {
|
|
552
|
+
let inspected: string | undefined;
|
|
553
|
+
chain(mkOk()).inspect((v) => { inspected = v; });
|
|
554
|
+
expect(inspected).toBe("hello");
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
it("inspectErr calls fn for err", () => {
|
|
558
|
+
let inspected: Err<TestErrors> | undefined;
|
|
559
|
+
chain(mkErr()).inspectErr((e) => { inspected = e; });
|
|
560
|
+
expect(inspected).toEqual({ type: "not_found", detail: "/path" });
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
it("isOk / isErr return correct booleans", () => {
|
|
564
|
+
expect(chain(mkOk()).isOk()).toBe(true);
|
|
565
|
+
expect(chain(mkOk()).isErr()).toBe(false);
|
|
566
|
+
expect(chain(mkErr()).isOk()).toBe(false);
|
|
567
|
+
expect(chain(mkErr()).isErr()).toBe(true);
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
it("isOkAnd / isErrAnd with predicates", () => {
|
|
571
|
+
expect(chain(mkOk()).isOkAnd((v) => v === "hello")).toBe(true);
|
|
572
|
+
expect(chain(mkOk()).isOkAnd((v) => v === "nope")).toBe(false);
|
|
573
|
+
expect(chain(mkErr()).isErrAnd((e) => e.type === "not_found")).toBe(true);
|
|
574
|
+
expect(chain(mkErr()).isErrAnd((e) => e.type === "invalid")).toBe(false);
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
it("ok() / err() extract values", () => {
|
|
578
|
+
expect(chain(mkOk()).ok()).toBe("hello");
|
|
579
|
+
expect(chain(mkOk()).err()).toBeNull();
|
|
580
|
+
expect(chain(mkErr()).ok()).toBeNull();
|
|
581
|
+
expect(chain(mkErr()).err()).toEqual({ type: "not_found", detail: "/path" });
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
it("mapOr / mapOrElse work through chain", () => {
|
|
585
|
+
expect(chain(mkOk()).mapOr(0, (v) => v.length)).toBe(5);
|
|
586
|
+
expect(chain(mkErr()).mapOr(0, (v) => v.length)).toBe(0);
|
|
587
|
+
expect(chain(mkOk()).mapOrElse(() => 0, (v) => v.length)).toBe(5);
|
|
588
|
+
expect(chain(mkErr()).mapOrElse((e) => (e.type === "not_found" ? -1 : -2), (v) => v.length)).toBe(-1);
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
it("flatten unwraps nested result", () => {
|
|
592
|
+
const nested: Result<Result<string, TestErrors>, TestErrors> = ok(r.ok("inner"));
|
|
593
|
+
const c = chain(nested).flatten();
|
|
594
|
+
expect(c.unwrap()).toBe("inner");
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
it("result property exposes the raw result", () => {
|
|
598
|
+
const c = chain(mkOk());
|
|
599
|
+
expect(c.result).toEqual({ ok: true, value: "hello" });
|
|
600
|
+
});
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
// --- tryCatchResultAsync / tryCatchResultPromise ---
|
|
604
|
+
|
|
605
|
+
describe("tryCatchResultAsync", () => {
|
|
606
|
+
it("returns ok on success", async () => {
|
|
607
|
+
const result = await resultFromThrowingAsyncFunction(
|
|
608
|
+
async () => 42,
|
|
609
|
+
(e) => err("fail", String(e))
|
|
610
|
+
);
|
|
611
|
+
expect(result.ok).toBe(true);
|
|
612
|
+
if (result.ok) expect(result.value).toBe(42);
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
it("returns err on throw", async () => {
|
|
616
|
+
const result = await resultFromThrowingAsyncFunction(
|
|
617
|
+
async () => { throw new Error("boom"); },
|
|
618
|
+
(e) => err("fail", String(e))
|
|
619
|
+
);
|
|
620
|
+
expect(result.ok).toBe(false);
|
|
621
|
+
if (!result.ok) expect(result.error.type).toBe("fail");
|
|
622
|
+
});
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
describe("tryCatchResultPromise", () => {
|
|
626
|
+
it("returns ok on resolve", async () => {
|
|
627
|
+
const result = await resultFromThrowingPromise(
|
|
628
|
+
Promise.resolve(42),
|
|
629
|
+
(e) => err("fail", String(e))
|
|
630
|
+
);
|
|
631
|
+
expect(result.ok).toBe(true);
|
|
632
|
+
if (result.ok) expect(result.value).toBe(42);
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
it("returns err on reject", async () => {
|
|
636
|
+
const result = await resultFromThrowingPromise(
|
|
637
|
+
Promise.reject(new Error("boom")),
|
|
638
|
+
(e) => err("fail", String(e))
|
|
639
|
+
);
|
|
640
|
+
expect(result.ok).toBe(false);
|
|
641
|
+
if (!result.ok) expect(result.error.type).toBe("fail");
|
|
642
|
+
});
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
// --- AsyncChain ---
|
|
646
|
+
|
|
647
|
+
describe("asyncChain", () => {
|
|
648
|
+
const asyncOk = () => Promise.resolve(mkOk());
|
|
649
|
+
const asyncErr = () => Promise.resolve(mkErr());
|
|
650
|
+
|
|
651
|
+
it("map transforms ok value", async () => {
|
|
652
|
+
const result = await asyncChain(asyncOk()).map((v) => v.length).unwrap();
|
|
653
|
+
expect(result).toBe(5);
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
it("andThen chains ok", async () => {
|
|
657
|
+
const result = await asyncChain(asyncOk())
|
|
658
|
+
.andThen(async (v) => r.ok(v + "!"))
|
|
659
|
+
.unwrap();
|
|
660
|
+
expect(result).toBe("hello!");
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
it("mapErr transforms error", async () => {
|
|
664
|
+
const e = await asyncChain(asyncErr())
|
|
665
|
+
.mapErr((e) => ({ type: "wrapped" as const, detail: e.type as string }))
|
|
666
|
+
.err();
|
|
667
|
+
expect(e).toEqual({ type: "wrapped", detail: "not_found" });
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
it("or / orElse work", async () => {
|
|
671
|
+
expect(
|
|
672
|
+
await asyncChain(asyncErr()).or(Promise.resolve(r.ok("other"))).unwrap()
|
|
673
|
+
).toBe("other");
|
|
674
|
+
expect(
|
|
675
|
+
await asyncChain(asyncErr())
|
|
676
|
+
.orElse(async (e) => r.ok(`recovered: ${e.type}`))
|
|
677
|
+
.unwrap()
|
|
678
|
+
).toBe("recovered: not_found");
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
it("unwrapOr / unwrapOrElse work", async () => {
|
|
682
|
+
expect(await asyncChain(asyncErr()).unwrapOr("default")).toBe("default");
|
|
683
|
+
expect(await asyncChain(asyncErr()).unwrapOrElse((e) => `err: ${e.type}`)).toBe("err: not_found");
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
it("expect / expectErr work", async () => {
|
|
687
|
+
expect(await asyncChain(asyncOk()).expect("msg")).toBe("hello");
|
|
688
|
+
await expect(asyncChain(asyncErr()).expect("msg")).rejects.toThrow("msg");
|
|
689
|
+
expect(await asyncChain(asyncErr()).expectErr("msg")).toEqual({ type: "not_found", detail: "/path" });
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
it("unwrapErr works", async () => {
|
|
693
|
+
expect(await asyncChain(asyncErr()).unwrapErr()).toEqual({ type: "not_found", detail: "/path" });
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
it("inspect / inspectErr work", async () => {
|
|
697
|
+
let okVal: string | undefined;
|
|
698
|
+
let errVal: Err<TestErrors> | undefined;
|
|
699
|
+
await asyncChain(asyncOk()).inspect((v) => { okVal = v; }).result;
|
|
700
|
+
await asyncChain(asyncErr()).inspectErr((e) => { errVal = e; }).result;
|
|
701
|
+
expect(okVal).toBe("hello");
|
|
702
|
+
expect(errVal).toEqual({ type: "not_found", detail: "/path" });
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
it("isOk / isErr / isOkAnd / isErrAnd work", async () => {
|
|
706
|
+
expect(await asyncChain(asyncOk()).isOk()).toBe(true);
|
|
707
|
+
expect(await asyncChain(asyncErr()).isErr()).toBe(true);
|
|
708
|
+
expect(await asyncChain(asyncOk()).isOkAnd((v) => v === "hello")).toBe(true);
|
|
709
|
+
expect(await asyncChain(asyncErr()).isErrAnd((e) => e.type === "not_found")).toBe(true);
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
it("ok / err extract values", async () => {
|
|
713
|
+
expect(await asyncChain(asyncOk()).ok()).toBe("hello");
|
|
714
|
+
expect(await asyncChain(asyncOk()).err()).toBeNull();
|
|
715
|
+
expect(await asyncChain(asyncErr()).ok()).toBeNull();
|
|
716
|
+
expect(await asyncChain(asyncErr()).err()).toEqual({ type: "not_found", detail: "/path" });
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
it("mapOr / mapOrElse work", async () => {
|
|
720
|
+
expect(await asyncChain(asyncOk()).mapOr(0, (v) => v.length)).toBe(5);
|
|
721
|
+
expect(await asyncChain(asyncErr()).mapOr(0, (v) => v.length)).toBe(0);
|
|
722
|
+
expect(await asyncChain(asyncOk()).mapOrElse(() => 0, (v) => v.length)).toBe(5);
|
|
723
|
+
expect(await asyncChain(asyncErr()).mapOrElse(() => -1, (v) => v.length)).toBe(-1);
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
it("result property exposes the raw promise", async () => {
|
|
727
|
+
const result = await asyncChain(asyncOk()).result;
|
|
728
|
+
expect(result).toEqual({ ok: true, value: "hello" });
|
|
729
|
+
});
|
|
730
|
+
});
|
|
731
|
+
|
|
732
|
+
// --- Type narrowing tests ---
|
|
733
|
+
|
|
734
|
+
describe("type narrowing", () => {
|
|
735
|
+
it("ok branch narrows to value access", () => {
|
|
736
|
+
const result: Result<number, { fail: string }> = ok(42);
|
|
737
|
+
if (result.ok) {
|
|
738
|
+
const v: number = result.value;
|
|
739
|
+
expect(v).toBe(42);
|
|
740
|
+
expect(result.error).toBeUndefined();
|
|
741
|
+
}
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
it("err branch narrows to error access", () => {
|
|
745
|
+
const result: Result<number, { fail: string }> = err("fail", "oops");
|
|
746
|
+
if (!result.ok) {
|
|
747
|
+
const e = result.error;
|
|
748
|
+
expect(e.type).toBe("fail");
|
|
749
|
+
expect(e.detail).toBe("oops");
|
|
750
|
+
expect(result.value).toBeUndefined();
|
|
751
|
+
}
|
|
752
|
+
});
|
|
753
|
+
|
|
754
|
+
it("resultIsOk narrows type", () => {
|
|
755
|
+
const result: Result<number, { fail: string }> = ok(10);
|
|
756
|
+
if (resultIsOk(result)) {
|
|
757
|
+
const v: number = result.value;
|
|
758
|
+
expect(v).toBe(10);
|
|
759
|
+
}
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
it("resultIsErr narrows type", () => {
|
|
763
|
+
const result: Result<number, { fail: string }> = err("fail", "oops");
|
|
764
|
+
if (resultIsErr(result)) {
|
|
765
|
+
expect(result.error.type).toBe("fail");
|
|
766
|
+
}
|
|
767
|
+
});
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
// --- Undetailed errors ---
|
|
771
|
+
|
|
772
|
+
describe("Undetailed errors", () => {
|
|
773
|
+
it("errConstructor works without detail for undefined-valued keys", () => {
|
|
774
|
+
type Errors = { timeout: undefined; cancelled: undefined };
|
|
775
|
+
const r2 = resultType<string, Errors>();
|
|
776
|
+
const result = r2.err("timeout");
|
|
777
|
+
expect(result.ok).toBe(false);
|
|
778
|
+
if (!result.ok) {
|
|
779
|
+
expect(result.error.type).toBe("timeout");
|
|
780
|
+
}
|
|
781
|
+
});
|
|
782
|
+
});
|