1import { Chunk, Equal, Equivalence, flow, identity, Option, pipe, Result, String as Str } from "effect"
2import { inspect } from "node:util"
3import { describe, it } from "vitest"
4import {
5 assertFailure,
6 assertFalse,
7 assertNone,
8 assertSome,
9 assertSuccess,
10 assertTrue,
11 deepStrictEqual,
12 strictEqual,
13 throws
14} from "./utils/assert.ts"
15
16describe("Result", () => {
17 describe("Constructors", () => {
18 it("succeed creates a Success value", () => {
19 const result = Result.succeed(42)
20 assertTrue(Result.isSuccess(result))
21 strictEqual(result.success, 42)
22 })
23
24 it("fail creates a Failure value", () => {
25 const result = Result.fail("err")
26 assertTrue(Result.isFailure(result))
27 strictEqual(result.failure, "err")
28 })
29
30 it("void", () => {
31 deepStrictEqual(Result.void, Result.succeed(undefined))
32 })
33
34 it("try", () => {
35 deepStrictEqual(Result.try(() => 1), Result.succeed(1))
36 deepStrictEqual(
37 Result.try(() => {
38 throw "b"
39 }),
40 Result.fail("b")
41 )
42 deepStrictEqual(Result.try({ try: () => 1, catch: (e) => new Error(String(e)) }), Result.succeed(1))
43 deepStrictEqual(
44 Result.try({
45 try: () => {
46 throw "b"
47 },
48 catch: (e) => new Error(String(e))
49 }),
50 Result.fail(new Error("b"))
51 )
52 })
53
54 it("fromNullishOr", () => {
55 deepStrictEqual(Result.fromNullishOr(null, () => "fallback"), Result.fail("fallback"))
56 deepStrictEqual(Result.fromNullishOr(undefined, () => "fallback"), Result.fail("fallback"))
57 deepStrictEqual(Result.fromNullishOr(1, () => "fallback"), Result.succeed(1))
58 // data-last
59 deepStrictEqual(pipe(null as string | null, Result.fromNullishOr(() => "fallback")), Result.fail("fallback"))
60 deepStrictEqual(pipe(1 as number | null, Result.fromNullishOr(() => "fallback")), Result.succeed(1))
61 })
62
63 it("fromOption", () => {
64 deepStrictEqual(Result.fromOption(Option.none(), () => "none"), Result.fail("none"))
65 deepStrictEqual(Result.fromOption(Option.some(1), () => "none"), Result.succeed(1))
66 // data-last
67 deepStrictEqual(pipe(Option.none(), Result.fromOption(() => "none")), Result.fail("none"))
68 deepStrictEqual(pipe(Option.some(1), Result.fromOption(() => "none")), Result.succeed(1))
69 })
70
71 it("succeedNone", () => {
72 deepStrictEqual(Result.succeedNone, Result.succeed(Option.none()))
73 })
74
75 it("succeedSome", () => {
76 deepStrictEqual(Result.succeedSome(1), Result.succeed(Option.some(1)))
77 })
78 })
79
80 describe("Methods", () => {
81 it("toString", () => {
82 strictEqual(
83 String(Result.succeed(1)),
84 `success(1)`
85 )
86 strictEqual(
87 String(Result.fail("e")),
88 `failure("e")`
89 )
90 strictEqual(
91 String(Result.succeed(Chunk.make(1, 2, 3))),
92 `success(Chunk([1,2,3]))`
93 )
94 strictEqual(
95 String(Result.fail(Chunk.make(1, 2, 3))),
96 `failure(Chunk([1,2,3]))`
97 )
98 strictEqual(
99 String(Result.succeed(Option.some(1))),
100 `success(some(1))`
101 )
102 strictEqual(
103 String(Result.fail(Option.none())),
104 `failure(none())`
105 )
106 })
107
108 it("toJSON", () => {
109 deepStrictEqual(Result.succeed(1).toJSON(), { _id: "Result", _tag: "Success", value: 1 })
110 deepStrictEqual(Result.fail("e").toJSON(), { _id: "Result", _tag: "Failure", failure: "e" })
111 })
112
113 it("inspect", () => {
114 deepStrictEqual(inspect(Result.succeed(1)), inspect({ _id: "Result", _tag: "Success", value: 1 }))
115 deepStrictEqual(inspect(Result.fail("e")), inspect({ _id: "Result", _tag: "Failure", failure: "e" }))
116 })
117
118 it("Equal trait compares matching tags and values", () => {
119 assertTrue(Equal.equals(Result.succeed(1), Result.succeed(1)))
120 assertTrue(Equal.equals(Result.fail("e"), Result.fail("e")))
121 assertFalse(Equal.equals(Result.succeed(1), Result.fail("e")))
122 assertFalse(Equal.equals(Result.fail("e"), Result.succeed(1)))
123 })
124
125 it("pipe composes Result operations", () => {
126 assertSuccess(Result.succeed(1).pipe(Result.map((n) => n + 1)), 2)
127 })
128 })
129
130 describe("Type Guards", () => {
131 it("isResult", () => {
132 assertTrue(pipe(Result.succeed(1), Result.isResult))
133 assertTrue(pipe(Result.fail("e"), Result.isResult))
134 assertFalse(pipe(Option.some(1), Result.isResult))
135 })
136
137 it("isFailure", () => {
138 assertFalse(Result.isFailure(Result.succeed(1)))
139 assertTrue(Result.isFailure(Result.fail(1)))
140 })
141
142 it("isSuccess", () => {
143 assertTrue(Result.isSuccess(Result.succeed(1)))
144 assertFalse(Result.isSuccess(Result.fail(1)))
145 })
146 })
147
148 describe("Getters", () => {
149 it("getSuccess", () => {
150 assertSome(pipe(Result.succeed(1), Result.getSuccess), 1)
151 assertNone(pipe(Result.fail("a"), Result.getSuccess))
152 })
153
154 it("getFailure", () => {
155 assertNone(pipe(Result.succeed(1), Result.getFailure))
156 assertSome(pipe(Result.fail("e"), Result.getFailure), "e")
157 })
158
159 it("getOrElse", () => {
160 strictEqual(Result.getOrElse(Result.succeed(1), (error) => error + "!"), 1)
161 strictEqual(Result.getOrElse(Result.fail("not a number"), (error) => error + "!"), "not a number!")
162 // data-last
163 strictEqual(pipe(Result.succeed(1), Result.getOrElse((error) => error + "!")), 1)
164 strictEqual(pipe(Result.fail("not a number"), Result.getOrElse((error) => error + "!")), "not a number!")
165 })
166
167 it("getOrNull", () => {
168 strictEqual(Result.getOrNull(Result.succeed(1)), 1)
169 strictEqual(Result.getOrNull(Result.fail("a")), null)
170 })
171
172 it("getOrUndefined", () => {
173 strictEqual(Result.getOrUndefined(Result.succeed(1)), 1)
174 strictEqual(Result.getOrUndefined(Result.fail("a")), undefined)
175 })
176
177 it("getOrThrowWith", () => {
178 strictEqual(pipe(Result.succeed(1), Result.getOrThrowWith((e) => new Error(`Unexpected Err: ${e}`))), 1)
179 throws(() => pipe(Result.fail("e"), Result.getOrThrowWith((e) => new Error(`Unexpected Err: ${e}`))))
180 // data-first
181 strictEqual(Result.getOrThrowWith(Result.succeed(1), (e) => new Error(`Unexpected Err: ${e}`)), 1)
182 throws(() => Result.getOrThrowWith(Result.fail("e"), (e) => new Error(`Unexpected Err: ${e}`)))
183 })
184
185 it("getOrThrow", () => {
186 strictEqual(pipe(Result.succeed(1), Result.getOrThrow), 1)
187 throws(() => pipe(Result.fail(new Error("e")), Result.getOrThrow), new Error("e"))
188 })
189
190 it("merge", () => {
191 deepStrictEqual(Result.merge(Result.succeed(1)), 1)
192 deepStrictEqual(Result.merge(Result.fail("a")), "a")
193 })
194 })
195
196 describe("Mapping", () => {
197 it("map", () => {
198 const f = Result.map(Str.length)
199 const failure = Result.fail("s")
200 assertSuccess(pipe(Result.succeed("abc"), f), 3)
201 strictEqual(pipe(failure, f), failure)
202 // data-first
203 assertSuccess(Result.map(Result.succeed("abc"), Str.length), 3)
204 strictEqual(Result.map(failure, Str.length), failure)
205 })
206
207 it("mapBoth", () => {
208 const f = Result.mapBoth({
209 onFailure: Str.length,
210 onSuccess: (n: number) => n > 2
211 })
212 assertSuccess(pipe(Result.succeed(1), f), false)
213 assertFailure(pipe(Result.fail("a"), f), 1)
214 // data-first
215 assertSuccess(
216 Result.mapBoth(Result.succeed(1), {
217 onFailure: Str.length,
218 onSuccess: (n: number) => n > 2
219 }),
220 false
221 )
222 assertFailure(
223 Result.mapBoth(Result.fail("a"), {
224 onFailure: Str.length,
225 onSuccess: (n: number) => n > 2
226 }),
227 1
228 )
229 })
230
231 it("mapError", () => {
232 const f = Result.mapError((n: number) => n * 2)
233 const success = Result.succeed("a")
234 strictEqual(pipe(success, f), success)
235 assertFailure(pipe(Result.fail(1), f), 2)
236 // data-first
237 strictEqual(Result.mapError(success, (n: number) => n * 2), success)
238 assertFailure(Result.mapError(Result.fail(1), (n) => n * 2), 2)
239 })
240
241 it("tap", () => {
242 let sideEffect = 0
243 const success = pipe(
244 Result.succeed(42),
245 Result.tap((n) => {
246 sideEffect = n
247 })
248 )
249 assertSuccess(success, 42)
250 strictEqual(sideEffect, 42)
251
252 // failure path: side effect should not run
253 let failureSideEffect = 0
254 const failure = pipe(
255 Result.fail("err") as Result.Result<number, string>,
256 Result.tap((n) => {
257 failureSideEffect = n
258 })
259 )
260 assertFailure(failure, "err")
261 strictEqual(failureSideEffect, 0)
262
263 // data-first
264 let sideEffect2 = 0
265 const success2 = Result.tap(Result.succeed(10), (n) => {
266 sideEffect2 = n
267 })
268 assertSuccess(success2, 10)
269 strictEqual(sideEffect2, 10)
270 })
271 })
272
273 describe("Pattern Matching", () => {
274 it("match handles failure and success cases", () => {
275 const onFailure = (s: string) => `failure${s.length}`
276 const onSuccess = (s: string) => `success${s.length}`
277 const match = Result.match({ onFailure, onSuccess })
278 strictEqual(match(Result.fail("abc")), "failure3")
279 strictEqual(match(Result.succeed("abc")), "success3")
280 // data-first
281 strictEqual(Result.match(Result.fail("abc"), { onFailure, onSuccess }), "failure3")
282 strictEqual(Result.match(Result.succeed("abc"), { onFailure, onSuccess }), "success3")
283 })
284 })
285
286 describe("Utils", () => {
287 it("flip", () => {
288 assertFailure(Result.flip(Result.succeed("a")), "a")
289 assertSuccess(Result.flip(Result.fail("b")), "b")
290 })
291
292 it("liftPredicate", () => {
293 const isPositivePredicate = (n: number) => n > 0
294 const onPositivePredicateError = (n: number) => `${n} is not positive`
295 const isNumberRefinement = (n: string | number): n is number => typeof n === "number"
296 const onNumberRefinementError = (n: string | number) => `${n} is not a number`
297
298 assertSuccess(
299 pipe(1, Result.liftPredicate(isPositivePredicate, onPositivePredicateError)),
300 1
301 )
302 assertFailure(
303 pipe(-1, Result.liftPredicate(isPositivePredicate, onPositivePredicateError)),
304 "-1 is not positive"
305 )
306 assertSuccess(
307 pipe(1, Result.liftPredicate(isNumberRefinement, onNumberRefinementError)),
308 1
309 )
310 assertFailure(
311 pipe("string", Result.liftPredicate(isNumberRefinement, onNumberRefinementError)),
312 "string is not a number"
313 )
314
315 assertSuccess(
316 Result.liftPredicate(1, isPositivePredicate, onPositivePredicateError),
317 1
318 )
319 assertFailure(
320 Result.liftPredicate(-1, isPositivePredicate, onPositivePredicateError),
321 "-1 is not positive"
322 )
323 assertSuccess(
324 Result.liftPredicate(1, isNumberRefinement, onNumberRefinementError),
325 1
326 )
327 assertFailure(
328 Result.liftPredicate("string", isNumberRefinement, onNumberRefinementError),
329 "string is not a number"
330 )
331 })
332 })
333
334 describe("Filtering", () => {
335 it("filterOrFail", () => {
336 deepStrictEqual(Result.filterOrFail(Result.succeed(1), (n) => n > 0, () => "a"), Result.succeed(1))
337 deepStrictEqual(Result.filterOrFail(Result.succeed(1), (n) => n > 1, () => "a"), Result.fail("a"))
338 deepStrictEqual(Result.filterOrFail(Result.fail(1), (n) => n > 0, () => "a"), Result.fail(1))
339
340 deepStrictEqual(Result.succeed(1).pipe(Result.filterOrFail((n) => n > 0, () => "a")), Result.succeed(1))
341 deepStrictEqual(Result.succeed(1).pipe(Result.filterOrFail((n) => n > 1, () => "a")), Result.fail("a"))
342 deepStrictEqual(Result.fail(1).pipe(Result.filterOrFail((n) => n > 0, () => "a")), Result.fail(1))
343 })
344 })
345
346 describe("Equivalence", () => {
347 it("makeEquivalence compares successes and failures by channel", () => {
348 const isEquivalent = Result.makeEquivalence(
349 Equivalence.strictEqual<number>(),
350 Equivalence.strictEqual<string>()
351 )
352 deepStrictEqual(isEquivalent(Result.succeed(1), Result.succeed(1)), true)
353 deepStrictEqual(isEquivalent(Result.succeed(1), Result.succeed(2)), false)
354 deepStrictEqual(isEquivalent(Result.succeed(1), Result.fail("foo")), false)
355 deepStrictEqual(isEquivalent(Result.fail("foo"), Result.fail("foo")), true)
356 deepStrictEqual(isEquivalent(Result.fail("foo"), Result.fail("bar")), false)
357 deepStrictEqual(isEquivalent(Result.fail("foo"), Result.succeed(1)), false)
358 })
359 })
360
361 describe("Sequencing", () => {
362 it("flatMap", () => {
363 const f = Result.flatMap(flow(Str.length, Result.succeed))
364 assertSuccess(pipe(Result.succeed("abc"), f), 3)
365 assertFailure(pipe(Result.fail("maError"), f), "maError")
366 // data-first
367 assertSuccess(Result.flatMap(Result.succeed("abc"), flow(Str.length, Result.succeed)), 3)
368 assertFailure(Result.flatMap(Result.fail("maError"), flow(Str.length, Result.succeed)), "maError")
369 })
370
371 it("andThen replaces successes with Result or plain values", () => {
372 assertSuccess(pipe(Result.succeed(1), Result.andThen(() => Result.succeed(2))), 2)
373 assertSuccess(pipe(Result.succeed(1), Result.andThen(Result.succeed(2))), 2)
374 assertSuccess(pipe(Result.succeed(1), Result.andThen(2)), 2)
375 assertSuccess(pipe(Result.succeed(1), Result.andThen(() => 2)), 2)
376 assertSuccess(pipe(Result.succeed(1), Result.andThen((a) => a)), 1)
377 assertSuccess(Result.andThen(Result.succeed(1), () => Result.succeed(2)), 2)
378 assertSuccess(Result.andThen(Result.succeed(1), Result.succeed(2)), 2)
379 assertSuccess(Result.andThen(Result.succeed(1), () => 2), 2)
380 assertSuccess(Result.andThen(Result.succeed(1), 2), 2)
381 assertSuccess(Result.andThen(Result.succeed(1), (a) => a), 1)
382 })
383
384 it("all", () => {
385 // tuples and arrays
386 assertSuccess(Result.all([]), [])
387 assertSuccess(Result.all([Result.succeed(1)]), [1])
388 assertSuccess(Result.all([Result.succeed(1), Result.succeed(true)]), [1, true])
389 assertFailure(Result.all([Result.succeed(1), Result.fail("e")]), "e")
390 // structs and records
391 assertSuccess(Result.all({}), {})
392 assertSuccess(Result.all({ a: Result.succeed(1) }), { a: 1 })
393 assertSuccess(Result.all({ a: Result.succeed(1), b: Result.succeed(true) }), { a: 1, b: true })
394 assertFailure(Result.all({ a: Result.succeed(1), b: Result.fail("e") }), "e")
395 // iterables
396 const iterable = function*() {
397 yield Result.succeed(1)
398 yield Result.succeed(2)
399 yield Result.succeed(3)
400 }
401 assertSuccess(Result.all(iterable()), [1, 2, 3])
402
403 const iterableWithFailure = function*() {
404 yield Result.succeed(1)
405 yield Result.fail("e")
406 yield Result.succeed(3)
407 }
408 assertFailure(Result.all(iterableWithFailure()), "e")
409 })
410 })
411
412 describe("Error Handling", () => {
413 it("orElse recovers failures and preserves successes", () => {
414 assertSuccess(pipe(Result.succeed(1), Result.orElse(() => Result.succeed(2))), 1)
415 assertSuccess(pipe(Result.succeed(1), Result.orElse(() => Result.fail("b"))), 1)
416 assertSuccess(pipe(Result.fail("a"), Result.orElse(() => Result.succeed(2))), 2)
417 assertFailure(pipe(Result.fail("a"), Result.orElse(() => Result.fail("b"))), "b")
418 // data-first
419 assertSuccess(Result.orElse(Result.succeed(1), () => Result.succeed(2)), 1)
420 assertSuccess(Result.orElse(Result.fail("a"), () => Result.succeed(2)), 2)
421 assertFailure(Result.orElse(Result.fail("a"), () => Result.fail("b")), "b")
422 })
423 })
424
425 describe("Do Notation", () => {
426 it("Do", () => {
427 assertSuccess(Result.Do, {})
428 })
429
430 it("bindTo", () => {
431 assertSuccess(pipe(Result.succeed(1), Result.bindTo("a")), { a: 1 })
432 assertFailure(pipe(Result.fail("left"), Result.bindTo("a")), "left")
433
434 assertSuccess(pipe(Result.succeed(1), Result.bindTo("__proto__")), { ["__proto__"]: 1 })
435 })
436
437 it("bind", () => {
438 assertSuccess(pipe(Result.succeed(1), Result.bindTo("a"), Result.bind("b", ({ a }) => Result.succeed(a + 1))), {
439 a: 1,
440 b: 2
441 })
442 assertFailure(
443 pipe(Result.succeed(1), Result.bindTo("a"), Result.bind("b", () => Result.fail("left"))),
444 "left"
445 )
446 assertFailure(
447 pipe(Result.fail("left"), Result.bindTo("a"), Result.bind("b", () => Result.succeed(2))),
448 "left"
449 )
450 assertSuccess(pipe(Result.Do, Result.bind("__proto__", () => Result.succeed(1))), { ["__proto__"]: 1 })
451 })
452
453 it("let", () => {
454 assertSuccess(pipe(Result.succeed(1), Result.bindTo("a"), Result.let("b", ({ a }) => a + 1)), { a: 1, b: 2 })
455 assertFailure(
456 pipe(Result.fail("left"), Result.bindTo("a"), Result.let("b", () => 2)),
457 "left"
458 )
459 assertSuccess(pipe(Result.Do, Result.let("__proto__", () => 1)), { ["__proto__"]: 1 })
460 })
461 })
462
463 describe("Generators", () => {
464 it("gen", () => {
465 const a = Result.gen(function*() {
466 const x = yield* Result.succeed(1)
467 const y = yield* Result.succeed(2)
468 return x + y
469 })
470 const b = Result.gen(function*() {
471 return 10
472 })
473 const c = Result.gen(function*() {
474 yield* Result.succeed(1)
475 yield* Result.succeed(2)
476 })
477 const d = Result.gen(function*() {
478 yield* Result.succeed(1)
479 return yield* Result.succeed(2)
480 })
481 const e = Result.gen(function*() {
482 yield* Result.succeed(1)
483 yield* Result.fail("err")
484 return yield* Result.succeed(2)
485 })
486 const f = Result.gen(function*() {
487 yield* Result.fail("err")
488 })
489 const g = Result.gen({ context: "testContext" as const }, function*() {
490 return yield* Result.succeed(this.context)
491 })
492
493 assertSuccess(a, 3)
494 assertSuccess(b, 10)
495 assertSuccess(c, undefined)
496 assertSuccess(d, 2)
497 assertFailure(e, "err")
498 assertFailure(f, "err")
499 assertSuccess(g, "testContext")
500 })
501 })
502
503 describe("Transposing", () => {
504 it("transposeOption", () => {
505 assertSuccess(Result.transposeOption(Option.some(Result.succeed(1))), Option.some(1))
506 assertSuccess(Result.transposeOption(Option.none()), Option.none())
507 assertFailure(Result.transposeOption(Option.some(Result.fail("e"))), "e")
508 })
509
510 it("transposeMapOption", () => {
511 assertSuccess(Result.transposeMapOption(Option.some(Result.succeed(1)), identity), Option.some(1))
512 assertSuccess(Result.transposeMapOption(Option.none(), identity), Option.none())
513 assertFailure(Result.transposeMapOption(Option.some(Result.fail("e")), identity), "e")
514 // data-last
515 assertSuccess(pipe(Option.some(1), Result.transposeMapOption(Result.succeed)), Option.some(1))
516 assertSuccess(pipe(Option.none<number>(), Result.transposeMapOption(Result.succeed)), Option.none())
517 })
518 })
519})