|
| 1 | +import { describe, test, expectTypeOf, assertType } from 'vitest'; |
| 2 | +import { |
| 3 | + Some, |
| 4 | + None, |
| 5 | + type Option, |
| 6 | + type SomeOption, |
| 7 | + type NoneOption |
| 8 | +} from '../src/option'; |
| 9 | +import { |
| 10 | + Ok, |
| 11 | + Err, |
| 12 | + type Result, |
| 13 | + type OkResult, |
| 14 | + type ErrResult |
| 15 | +} from '../src/result'; |
| 16 | +import { type AsyncOption } from '../src/async-option'; |
| 17 | +import { type AsyncResult } from '../src/async-result'; |
| 18 | + |
| 19 | +describe('Option types', () => { |
| 20 | + test('Some and None constructors map correctly to Option<T>', () => { |
| 21 | + const someValue = Some(42); |
| 22 | + const noneValue = None<number>(); |
| 23 | + |
| 24 | + expectTypeOf(someValue).toEqualTypeOf<Option<number>>(); |
| 25 | + expectTypeOf(noneValue).toEqualTypeOf<Option<number>>(); |
| 26 | + |
| 27 | + // @ts-expect-error - NoneOption cannot be assigned to SomeOption |
| 28 | + const strictSome: SomeOption<number> = None<number>(); |
| 29 | + }); |
| 30 | + |
| 31 | + test('isSome and isNone type guard refinements', () => { |
| 32 | + const opt = Some(42) as Option<number>; |
| 33 | + |
| 34 | + if (opt.isSome()) expectTypeOf(opt).toEqualTypeOf<SomeOption<number>>(); |
| 35 | + else if (opt.isNone()) |
| 36 | + expectTypeOf(opt).toEqualTypeOf<NoneOption<number>>(); |
| 37 | + else expectTypeOf(opt).toEqualTypeOf<never>(); |
| 38 | + }); |
| 39 | + |
| 40 | + test('isSomeAnd performs user-defined type-guard narrowing', () => { |
| 41 | + const opt = Some<string | number>(42); |
| 42 | + const isString = (val: string | number): val is string => |
| 43 | + typeof val === 'string'; |
| 44 | + |
| 45 | + if (opt.isSomeAnd(isString)) |
| 46 | + expectTypeOf(opt).toEqualTypeOf<SomeOption<string>>(); |
| 47 | + }); |
| 48 | + |
| 49 | + test('Chaining methods (map, mapAsync, andThen, okOr)', () => { |
| 50 | + const opt = Some(42); |
| 51 | + |
| 52 | + expectTypeOf(opt.map((v) => v.toString())).toEqualTypeOf< |
| 53 | + Option<string> |
| 54 | + >(); |
| 55 | + |
| 56 | + expectTypeOf(opt.mapAsync(async (v) => v.toString())).toEqualTypeOf< |
| 57 | + AsyncOption<string> |
| 58 | + >(); |
| 59 | + |
| 60 | + expectTypeOf(opt.andThen((v) => Some(v > 0))).toEqualTypeOf< |
| 61 | + Option<boolean> |
| 62 | + >(); |
| 63 | + |
| 64 | + expectTypeOf(opt.okOr('error_msg')).toEqualTypeOf< |
| 65 | + Result<number, string> |
| 66 | + >(); |
| 67 | + }); |
| 68 | +}); |
| 69 | + |
| 70 | +describe('Result types', () => { |
| 71 | + test('Ok and Err map into variants of Result<T, E>', () => { |
| 72 | + const okVal = Ok(100); |
| 73 | + const errVal = Err('failed'); |
| 74 | + |
| 75 | + expectTypeOf(okVal).toEqualTypeOf<Result<number, never>>(); |
| 76 | + expectTypeOf(errVal).toEqualTypeOf<Result<never, string>>(); |
| 77 | + |
| 78 | + assertType<Result<number, string>>(okVal); |
| 79 | + assertType<Result<number, string>>(errVal); |
| 80 | + }); |
| 81 | + |
| 82 | + test('isOk and isErr type guards split types correctly', () => { |
| 83 | + const res = Ok(42) as Result<number, string>; |
| 84 | + |
| 85 | + if (res.isOk()) |
| 86 | + expectTypeOf(res).toEqualTypeOf<OkResult<number, never>>(); |
| 87 | + else if (res.isErr()) |
| 88 | + expectTypeOf(res).toEqualTypeOf<ErrResult<never, string>>(); |
| 89 | + }); |
| 90 | + |
| 91 | + test('isOkAnd user-defined type guard narrowing', () => { |
| 92 | + const res = Ok<string | number>(42) as Result<string | number, string>; |
| 93 | + const isNumber = (val: string | number): val is number => |
| 94 | + typeof val === 'number'; |
| 95 | + |
| 96 | + if (res.isOkAnd(isNumber)) |
| 97 | + expectTypeOf(res).toEqualTypeOf<OkResult<number, string>>(); |
| 98 | + }); |
| 99 | + |
| 100 | + test('Chaining operations (map, mapErr, mapAsync, ok, err)', () => { |
| 101 | + const res = Ok(42) as Result<number, string>; |
| 102 | + |
| 103 | + expectTypeOf(res.map((v) => v * 2)).toEqualTypeOf< |
| 104 | + Result<number, string> |
| 105 | + >(); |
| 106 | + |
| 107 | + expectTypeOf(res.mapErr((e) => new Error(e))).toEqualTypeOf< |
| 108 | + Result<number, Error> |
| 109 | + >(); |
| 110 | + |
| 111 | + expectTypeOf(res.mapAsync(async (v) => v.toString())).toEqualTypeOf< |
| 112 | + AsyncResult<string, string> |
| 113 | + >(); |
| 114 | + |
| 115 | + expectTypeOf(res.ok()).toEqualTypeOf<Option<number>>(); |
| 116 | + expectTypeOf(res.err()).toEqualTypeOf<Option<string>>(); |
| 117 | + }); |
| 118 | +}); |
| 119 | + |
| 120 | +describe('Async Wrappers (AsyncOption & AsyncResult)', () => { |
| 121 | + test('AsyncOption promise-like structural behavior', () => { |
| 122 | + const asyncOpt = {} as AsyncOption<number>; |
| 123 | + |
| 124 | + expectTypeOf(asyncOpt).toExtend<PromiseLike<Option<number>>>(); |
| 125 | + |
| 126 | + expectTypeOf(asyncOpt.isSome()).toEqualTypeOf<Promise<boolean>>(); |
| 127 | + expectTypeOf(asyncOpt.unwrap()).toEqualTypeOf<Promise<number>>(); |
| 128 | + expectTypeOf(asyncOpt.map((v) => v.toString())).toEqualTypeOf< |
| 129 | + AsyncOption<string> |
| 130 | + >(); |
| 131 | + expectTypeOf(asyncOpt.okOr('err')).toEqualTypeOf< |
| 132 | + AsyncResult<number, string> |
| 133 | + >(); |
| 134 | + }); |
| 135 | + |
| 136 | + test('AsyncResult promise-like structural behavior', () => { |
| 137 | + const asyncRes = {} as AsyncResult<number, string>; |
| 138 | + |
| 139 | + expectTypeOf(asyncRes).toExtend<PromiseLike<Result<number, string>>>(); |
| 140 | + |
| 141 | + expectTypeOf(asyncRes.isOk()).toEqualTypeOf<Promise<boolean>>(); |
| 142 | + expectTypeOf(asyncRes.map((v) => v * 2)).toEqualTypeOf< |
| 143 | + AsyncResult<number, string> |
| 144 | + >(); |
| 145 | + expectTypeOf(asyncRes.ok()).toEqualTypeOf<AsyncOption<number>>(); |
| 146 | + expectTypeOf(asyncRes.err()).toEqualTypeOf<AsyncOption<string>>(); |
| 147 | + }); |
| 148 | +}); |
0 commit comments