1import {
2 assertFalse,
3 assertNone,
4 assertSome,
5 assertTrue,
6 deepStrictEqual,
7 strictEqual,
8 throws
9} from "@effect/vitest/utils"
10import { Chunk, Equal, Equivalence, Hash, Number, Number as Num, Option, pipe, Result, String as Str } from "effect"
11import { describe, it } from "vitest"
12
13const gt2 = (n: number): boolean => n > 2
14
15describe("Option", () => {
16 it("makeReducer", () => {
17 const R = Option.makeReducer(Number.ReducerSum)
18
19 deepStrictEqual(R.combine(Option.some(1), Option.some(2)), Option.some(3))
20 deepStrictEqual(R.combine(Option.some(1), Option.none()), Option.some(1))
21 deepStrictEqual(R.combine(Option.none(), Option.some(2)), Option.some(2))
22 deepStrictEqual(R.combine(Option.none(), Option.none()), Option.none())
23 })
24
25 it("makeReducerFailFast", () => {
26 const R = Option.makeReducerFailFast(Number.ReducerSum)
27
28 deepStrictEqual(R.combine(Option.some(1), Option.some(2)), Option.some(3))
29 deepStrictEqual(R.combine(Option.some(1), Option.none()), Option.none())
30 deepStrictEqual(R.combine(Option.none(), Option.some(2)), Option.none())
31 deepStrictEqual(R.combine(Option.none(), Option.none()), Option.none())
32
33 deepStrictEqual(R.combine(Option.none(), R.initialValue), Option.none())
34 deepStrictEqual(R.combine(R.initialValue, Option.none()), Option.none())
35 deepStrictEqual(R.combine(Option.some(1), R.initialValue), Option.some(1))
36 deepStrictEqual(R.combine(R.initialValue, Option.some(1)), Option.some(1))
37 })
38
39 it("gen", () => {
40 const a = Option.gen(function*() {
41 const x = yield* Option.some(1)
42 const y = yield* Option.some(2)
43 return x + y
44 })
45 const b = Option.gen(function*() {
46 return 10
47 })
48 const c = Option.gen(function*() {
49 yield* Option.some(1)
50 yield* Option.some(2)
51 })
52 const d = Option.gen(function*() {
53 yield* Option.some(1)
54 return yield* Option.some(2)
55 })
56 const e = Option.gen(function*() {
57 yield* Option.some(1)
58 yield* Option.none()
59 return yield* Option.some(2)
60 })
61 const f = Option.gen(function*() {
62 yield* Option.none()
63 })
64 const g = Option.gen({ ctx: "testContext" as const }, function*() {
65 return yield* Option.some(this.ctx)
66 })
67
68 assertSome(a, 3)
69 assertSome(b, 10)
70 assertSome(c, undefined)
71 assertSome(d, 2)
72 assertNone(e)
73 assertNone(f)
74 assertSome(g, "testContext")
75 })
76
77 it("toString", () => {
78 strictEqual(
79 String(Option.none()),
80 `none()`
81 )
82 strictEqual(
83 String(Option.some(1)),
84 `some(1)`
85 )
86 strictEqual(
87 String(Option.some(Chunk.make(1, 2, 3))),
88 `some(Chunk([1,2,3]))`
89 )
90 })
91
92 it("toJSON", () => {
93 deepStrictEqual(Option.none().toJSON(), { _id: "Option", _tag: "None" })
94 deepStrictEqual(Option.some(1).toJSON(), { _id: "Option", _tag: "Some", value: 1 })
95 })
96
97 it("inspect", () => {
98 if (typeof window !== "undefined") {
99 return
100 }
101 // eslint-disable-next-line @typescript-eslint/no-require-imports
102 const { inspect } = require("node:util")
103 deepStrictEqual(inspect(Option.none()), inspect({ _id: "Option", _tag: "None" }))
104 deepStrictEqual(inspect(Option.some(1)), inspect({ _id: "Option", _tag: "Some", value: 1 }))
105 })
106
107 it("verify structural equality", () => {
108 assertTrue(Equal.equals(Option.some(1), Option.some(1)))
109 assertFalse(Equal.equals(Option.some(1), Option.some(2)))
110 assertTrue(Equal.equals(Option.none(), Option.none()))
111 })
112
113 it("verify hash equality", () => {
114 strictEqual(Hash.hash(Option.some(1)), Hash.hash(Option.some(1)))
115 strictEqual(Hash.hash(Option.some(1)) === Hash.hash(Option.some(2)), false)
116 strictEqual(Hash.hash(Option.none()), Hash.hash(Option.none()))
117 })
118
119 it("getSuccess", () => {
120 assertSome(Option.getSuccess(Result.succeed(1)), 1)
121 assertNone(Option.getSuccess(Result.fail("a")))
122 })
123
124 it("getFailure", () => {
125 assertNone(Option.getFailure(Result.succeed(1)))
126 assertSome(Option.getFailure(Result.fail("a")), "a")
127 })
128
129 it("toRefinement", () => {
130 const f = (
131 s: string | number
132 ): Option.Option<string> => (typeof s === "string" ? Option.some(s) : Option.none())
133 const isString = Option.toRefinement(f)
134
135 assertTrue(isString("s"))
136 assertFalse(isString(1))
137
138 type A = { readonly type: "A" }
139 type B = { readonly type: "B" }
140 type C = A | B
141 const isA = Option.toRefinement<C, A>((c) => (c.type === "A" ? Option.some(c) : Option.none()))
142
143 assertTrue(isA({ type: "A" }))
144 assertFalse(isA({ type: "B" }))
145 })
146
147 it("isOption", () => {
148 assertTrue(pipe(Option.some(1), Option.isOption))
149 assertTrue(pipe(Option.none(), Option.isOption))
150 assertFalse(pipe(Result.succeed(1), Option.isOption))
151 })
152
153 it("firstSomeOf", () => {
154 assertNone(Option.firstSomeOf([]))
155 assertSome(Option.firstSomeOf([Option.some(1)]), 1)
156 assertNone(Option.firstSomeOf([Option.none()]))
157 assertSome(
158 Option.firstSomeOf([Option.none(), Option.none(), Option.none(), Option.none(), Option.some(1)]),
159 1
160 )
161 assertNone(
162 Option.firstSomeOf([Option.none(), Option.none(), Option.none(), Option.none()])
163 )
164 })
165
166 it("orElseResult", () => {
167 assertSome(pipe(Option.some(1), Option.orElseResult(() => Option.some(2))), Result.fail(1))
168 assertSome(pipe(Option.some(1), Option.orElseResult(() => Option.none())), Result.fail(1))
169 assertSome(pipe(Option.none(), Option.orElseResult(() => Option.some(2))), Result.succeed(2))
170 assertNone(pipe(Option.none(), Option.orElseResult(() => Option.none())))
171 })
172
173 it("orElseSome", () => {
174 assertSome(pipe(Option.some(1), Option.orElseSome(() => 2)), 1)
175 assertSome(pipe(Option.none(), Option.orElseSome(() => 2)), 2)
176 })
177
178 it("getOrThrow", () => {
179 strictEqual(pipe(Option.some(1), Option.getOrThrow), 1)
180 throws(() => pipe(Option.none(), Option.getOrThrow), new Error("getOrThrow called on a None"))
181 })
182
183 it("getOrThrowWith", () => {
184 strictEqual(pipe(Option.some(1), Option.getOrThrowWith(() => new Error("Unexpected None"))), 1)
185 throws(
186 () => pipe(Option.none(), Option.getOrThrowWith(() => new Error("Unexpected None"))),
187 new Error("Unexpected None")
188 )
189 })
190
191 it("unit", () => {
192 assertSome(Option.void, undefined)
193 })
194
195 it("product", () => {
196 const product = Option.product
197 assertNone(product(Option.none(), Option.none()))
198 assertNone(product(Option.some(1), Option.none()))
199 assertNone(product(Option.none(), Option.some("a")))
200 assertSome(product(Option.some(1), Option.some("a")), [1, "a"])
201 })
202
203 it("productMany", () => {
204 const productMany = Option.productMany
205 assertNone(productMany(Option.none(), []))
206 assertSome(productMany(Option.some(1), []), [1])
207 assertNone(productMany(Option.some(1), [Option.none()]))
208 assertSome(productMany(Option.some(1), [Option.some(2)]), [1, 2])
209 })
210
211 it("fromIterable", () => {
212 assertNone(Option.fromIterable([]))
213 assertSome(Option.fromIterable(["a"]), "a")
214 })
215
216 it("map", () => {
217 assertSome(pipe(Option.some(2), Option.map((n) => n * 2)), 4)
218 assertNone(pipe(Option.none(), Option.map((n) => n * 2)))
219 })
220
221 it("flatMap", () => {
222 const f = (n: number) => Option.some(n * 2)
223 const g = () => Option.none()
224 assertSome(pipe(Option.some(1), Option.flatMap(f)), 2)
225 assertNone(pipe(Option.none(), Option.flatMap(f)))
226 assertNone(pipe(Option.some(1), Option.flatMap(g)))
227 assertNone(pipe(Option.none(), Option.flatMap(g)))
228 })
229
230 it("andThen", () => {
231 assertSome(pipe(Option.some(1), Option.andThen(() => Option.some(2))), 2)
232 assertSome(pipe(Option.some(1), Option.andThen(Option.some(2))), 2)
233 assertSome(pipe(Option.some(1), Option.andThen(2)), 2)
234 assertSome(pipe(Option.some(1), Option.andThen(() => 2)), 2)
235 assertSome(pipe(Option.some(1), Option.andThen((a) => a)), 1)
236 assertSome(Option.andThen(Option.some(1), () => Option.some(2)), 2)
237 assertSome(Option.andThen(Option.some(1), Option.some(2)), 2)
238 assertSome(Option.andThen(Option.some(1), 2), 2)
239 assertSome(Option.andThen(Option.some(1), () => 2), 2)
240 assertSome(Option.andThen(Option.some(1), (a) => a), 1)
241 })
242
243 it("orElse", () => {
244 const assertOrElse = (
245 a: Option.Option<number>,
246 b: Option.Option<number>,
247 expected: Option.Option<number>
248 ) => {
249 deepStrictEqual(pipe(a, Option.orElse(() => b)), expected)
250 }
251 assertOrElse(Option.some(1), Option.some(2), Option.some(1))
252 assertOrElse(Option.some(1), Option.none(), Option.some(1))
253 assertOrElse(Option.none(), Option.some(2), Option.some(2))
254 assertOrElse(Option.none(), Option.none(), Option.none())
255 })
256
257 it("partitionMap", () => {
258 const f = (n: number) => (gt2(n) ? Result.succeed(n + 1) : Result.fail(n - 1))
259 deepStrictEqual(pipe(Option.none(), Option.partitionMap(f)), [Option.none(), Option.none()])
260 deepStrictEqual(pipe(Option.some(1), Option.partitionMap(f)), [Option.none(), Option.some(0)])
261 deepStrictEqual(pipe(Option.some(3), Option.partitionMap(f)), [Option.some(4), Option.none()])
262 })
263
264 it("filterMap", () => {
265 const f = (n: number) => (gt2(n) ? Result.succeed(n + 1) : Result.failVoid)
266 assertNone(pipe(Option.none(), Option.filterMap(f)))
267 assertNone(pipe(Option.some(1), Option.filterMap(f)))
268 assertSome(pipe(Option.some(3), Option.filterMap(f)), 4)
269 })
270
271 it("match", () => {
272 const onNone = () => "none"
273 const onSome = (s: string) => `some${s.length}`
274 const match = Option.match({ onNone, onSome })
275 strictEqual(match(Option.none()), "none")
276 strictEqual(match(Option.some("abc")), "some3")
277 })
278
279 it("getOrElse", () => {
280 strictEqual(pipe(Option.some(1), Option.getOrElse(() => 0)), 1)
281 strictEqual(pipe(Option.none(), Option.getOrElse(() => 0)), 0)
282 })
283
284 it("getOrNull", () => {
285 strictEqual(Option.getOrNull(Option.none()), null)
286 strictEqual(Option.getOrNull(Option.some(1)), 1)
287 })
288
289 it("getOrUndefined", () => {
290 strictEqual(Option.getOrUndefined(Option.none()), undefined)
291 strictEqual(Option.getOrUndefined(Option.some(1)), 1)
292 })
293
294 it("makeOrder", () => {
295 const OS = Option.makeOrder(Str.Order)
296 strictEqual(OS(Option.none(), Option.none()), 0)
297 strictEqual(OS(Option.some("a"), Option.none()), 1)
298 strictEqual(OS(Option.none(), Option.some("a")), -1)
299 strictEqual(OS(Option.some("a"), Option.some("a")), 0)
300 strictEqual(OS(Option.some("a"), Option.some("b")), -1)
301 strictEqual(OS(Option.some("b"), Option.some("a")), 1)
302 })
303
304 it("flatMapNullishOr", () => {
305 interface X {
306 readonly a?: {
307 readonly b?: {
308 readonly c?: {
309 readonly d: number
310 }
311 }
312 }
313 }
314 const x1: X = { a: {} }
315 const x2: X = { a: { b: {} } }
316 const x3: X = { a: { b: { c: { d: 1 } } } }
317 assertNone(
318 pipe(
319 Option.fromNullishOr(x1.a),
320 Option.flatMapNullishOr((x) => x.b),
321 Option.flatMapNullishOr((x) => x.c),
322 Option.flatMapNullishOr((x) => x.d)
323 )
324 )
325 assertNone(
326 pipe(
327 Option.fromNullishOr(x2.a),
328 Option.flatMapNullishOr((x) => x.b),
329 Option.flatMapNullishOr((x) => x.c),
330 Option.flatMapNullishOr((x) => x.d)
331 )
332 )
333 assertSome(
334 pipe(
335 Option.fromNullishOr(x3.a),
336 Option.flatMapNullishOr((x) => x.b),
337 Option.flatMapNullishOr((x) => x.c),
338 Option.flatMapNullishOr((x) => x.d)
339 ),
340 1
341 )
342 })
343
344 it("fromNullishOr", () => {
345 assertSome(Option.fromNullishOr(2), 2)
346 assertNone(Option.fromNullishOr(null))
347 assertNone(Option.fromNullishOr(undefined))
348 })
349
350 it("liftPredicate", () => {
351 assertNone(pipe(1, Option.liftPredicate(gt2)))
352 assertSome(pipe(3, Option.liftPredicate(gt2)), 3)
353 assertNone(Option.liftPredicate(1, gt2))
354 assertSome(Option.liftPredicate(3, gt2), 3)
355
356 type Direction = "asc" | "desc"
357 const isDirection = (s: string): s is Direction => s === "asc" || s === "desc"
358 assertSome(pipe("asc", Option.liftPredicate(isDirection)), "asc")
359 assertNone(pipe("foo", Option.liftPredicate(isDirection)))
360 assertSome(Option.liftPredicate("asc", isDirection), "asc")
361 assertNone(Option.liftPredicate("foo", isDirection))
362 })
363
364 it("containsWith", () => {
365 const containsWith = Option.containsWith<number>((self, that) => self % 2 === that % 2)
366 assertTrue(pipe(Option.some(2), containsWith(2)))
367 assertTrue(pipe(Option.some(4), containsWith(4)))
368 assertTrue(pipe(Option.some(1), containsWith(3)))
369
370 assertFalse(pipe(Option.none(), containsWith(2)))
371 assertFalse(pipe(Option.some(2), containsWith(1)))
372 })
373
374 it("contains", () => {
375 assertFalse(pipe(Option.none(), Option.contains(2)))
376 assertTrue(pipe(Option.some(2), Option.contains(2)))
377 assertFalse(pipe(Option.some(2), Option.contains(1)))
378 })
379
380 it("isNone", () => {
381 assertTrue(Option.isNone(Option.none()))
382 assertFalse(Option.isNone(Option.some(1)))
383 })
384
385 it("isSome", () => {
386 assertFalse(Option.isSome(Option.none()))
387 assertTrue(Option.isSome(Option.some(1)))
388 })
389
390 it("exists", () => {
391 const predicate = (a: number) => a === 2
392 assertFalse(pipe(Option.none(), Option.exists(predicate)))
393 assertFalse(pipe(Option.some(1), Option.exists(predicate)))
394 assertTrue(pipe(Option.some(2), Option.exists(predicate)))
395 })
396
397 it("liftNullishOr", () => {
398 const f = Option.liftNullishOr((n: number) => (n > 0 ? n : null))
399 assertSome(f(1), 1)
400 assertNone(f(-1))
401 })
402
403 it("liftThrowable", () => {
404 const parse = Option.liftThrowable(JSON.parse)
405 assertSome(parse("1"), 1)
406 assertNone(parse(""))
407 })
408
409 it("tap", () => {
410 assertNone(Option.tap(Option.none(), () => Option.none()))
411 assertNone(Option.tap(Option.some(1), () => Option.none()))
412 assertNone(Option.tap(Option.none(), (n) => Option.some(n * 2)))
413 assertSome(Option.tap(Option.some(1), (n) => Option.some(n * 2)), 1)
414 })
415
416 it("filter keeps Do notation records only when the predicate passes", () => {
417 assertSome(
418 pipe(
419 Option.Do,
420 Option.bind("x", () => Option.some("a")),
421 Option.bind("y", () => Option.some("a")),
422 Option.filter(({ x, y }) => x === y)
423 ),
424 { x: "a", y: "a" }
425 )
426 assertNone(
427 pipe(
428 Option.Do,
429 Option.bind("x", () => Option.some("a")),
430 Option.bind("y", () => Option.some("b")),
431 Option.filter(({ x, y }) => x === y)
432 )
433 )
434 })
435
436 it("zipWith", () => {
437 assertNone(pipe(Option.none(), Option.zipWith(Option.some(2), (a, b) => a + b)))
438 assertNone(pipe(Option.some(1), Option.zipWith(Option.none(), (a, b) => a + b)))
439 assertSome(pipe(Option.some(1), Option.zipWith(Option.some(2), (a, b) => a + b)), 3)
440 })
441
442 it("reduceCompact", () => {
443 const sumCompact = Option.reduceCompact(0, Num.sum)
444 strictEqual(sumCompact([]), 0)
445 strictEqual(sumCompact([Option.some(2), Option.some(3)]), 5)
446 strictEqual(sumCompact([Option.some(2), Option.none(), Option.some(3)]), 5)
447 })
448
449 it("makeEquivalence", () => {
450 const isEquivalent = Option.makeEquivalence(Equivalence.strictEqual<number>())
451 assertTrue(isEquivalent(Option.none(), Option.none()))
452 assertFalse(isEquivalent(Option.none(), Option.some(1)))
453 assertFalse(isEquivalent(Option.some(1), Option.none()))
454 assertFalse(isEquivalent(Option.some(2), Option.some(1)))
455 assertFalse(isEquivalent(Option.some(1), Option.some(2)))
456 assertTrue(isEquivalent(Option.some(2), Option.some(2)))
457 })
458
459 describe("all", () => {
460 it("tuple", () => {
461 assertSome(Option.all([]), [])
462 assertSome(Option.all([Option.some(1), Option.some("hello")]), [1, "hello"])
463 assertNone(Option.all([Option.some(1), Option.none()]))
464 })
465
466 it("iterable", () => {
467 assertSome(Option.all([]), [])
468 assertNone(Option.all([Option.none()]))
469 assertSome(Option.all([Option.some(1), Option.some(2)]), [1, 2])
470 assertSome(Option.all(new Set([Option.some(1), Option.some(2)])), [1, 2])
471 assertNone(Option.all([Option.some(1), Option.none()]))
472 })
473
474 it("struct", () => {
475 assertSome(
476 Option.all({ a: Option.some(1), b: Option.some("hello") }),
477 { a: 1, b: "hello" }
478 )
479 assertNone(Option.all({ a: Option.some(1), b: Option.none() }))
480 })
481 })
482
483 it(".pipe()", () => {
484 assertSome(Option.some(1).pipe(Option.map((n) => n + 1)), 2)
485 })
486
487 it("lift2", () => {
488 const f = Option.lift2((a: number, b: number): number => a + b)
489 assertNone(f(Option.none(), Option.none()))
490 assertNone(f(Option.some(1), Option.none()))
491 assertNone(f(Option.none(), Option.some(2)))
492 assertSome(f(Option.some(1), Option.some(2)), 3)
493 })
494
495 describe("do notation", () => {
496 it("Do", () => {
497 assertSome(Option.Do, {})
498 })
499
500 it("bindTo", () => {
501 assertSome(pipe(Option.some(1), Option.bindTo("a")), { a: 1 })
502 assertNone(pipe(Option.none(), Option.bindTo("a")))
503 assertSome(
504 pipe(
505 Option.some(1),
506 Option.bindTo("__proto__"),
507 Option.bind("x", () => Option.some(2))
508 ),
509 { x: 2, ["__proto__"]: 1 }
510 )
511 })
512
513 it("bind", () => {
514 assertSome(pipe(Option.some(1), Option.bindTo("a"), Option.bind("b", ({ a }) => Option.some(a + 1))), {
515 a: 1,
516 b: 2
517 })
518 assertNone(
519 pipe(Option.some(1), Option.bindTo("a"), Option.bind("b", () => Option.none()))
520 )
521 assertNone(
522 pipe(Option.none(), Option.bindTo("a"), Option.bind("b", () => Option.some(2)))
523 )
524 assertSome(
525 pipe(
526 Option.some(1),
527 Option.bindTo("a"),
528 Option.bind("__proto__", ({ a }) => Option.some(a + 1)),
529 Option.bind("b", ({ a }) => Option.some(a + 1))
530 ),
531 { a: 1, b: 2, ["__proto__"]: 2 }
532 )
533 })
534
535 it("let", () => {
536 assertSome(pipe(Option.some(1), Option.bindTo("a"), Option.let("b", ({ a }) => a + 1)), { a: 1, b: 2 })
537 assertNone(
538 pipe(Option.none(), Option.bindTo("a"), Option.let("b", () => 2))
539 )
540 assertSome(
541 pipe(
542 Option.some(1),
543 Option.bindTo("a"),
544 Option.let("__proto__", ({ a }) => a + 1),
545 Option.let("b", ({ a }) => a + 1)
546 ),
547 { a: 1, b: 2, ["__proto__"]: 2 }
548 )
549 })
550 })
551
552 it("as", () => {
553 assertNone(Option.none().pipe(Option.as("a")))
554 assertSome(Option.some(1).pipe(Option.as("a")), "a")
555
556 assertNone(Option.as(Option.none(), "a"))
557 assertSome(Option.as(Option.some(1), "a"), "a")
558 })
559
560 it("asVoid", () => {
561 assertNone(Option.none().pipe(Option.asVoid))
562 assertSome(Option.some(1).pipe(Option.asVoid), undefined)
563 })
564})