1/**
2 * Works with values that may be `undefined`.
3 *
4 * Use this module for plain TypeScript values of type `A | undefined` when
5 * `undefined` is the only absence marker. It is a small alternative to wrapping
6 * values in `Option` when your data already uses `undefined` to mean "no
7 * value". The module includes helpers for mapping defined values, matching both
8 * cases, throwing when a value is missing, adapting throwing functions, and
9 * building reducers or combiners.
10 *
11 * @since 4.0.0
12 */
13import * as Combiner from "./Combiner.ts"
14import type { LazyArg } from "./Function.ts"
15import { dual } from "./Function.ts"
16import * as Reducer from "./Reducer.ts"
17
18/**
19 * Maps a defined value with `f`, or returns `undefined` unchanged.
20 *
21 * **When to use**
22 *
23 * Use to apply a pure transformation to an `A | undefined` value while
24 * preserving `undefined` as absence.
25 *
26 * @see {@link match} when you need to handle the `undefined` case explicitly
27 *
28 * @category mapping
29 * @since 4.0.0
30 */
31export const map: {
32 <A, B>(f: (a: A) => B): (self: A | undefined) => B | undefined
33 <A, B>(self: A | undefined, f: (a: A) => B): B | undefined
34} = dual(2, (self, f) => (self === undefined ? undefined : f(self)))
35
36/**
37 * Pattern matches on an `A | undefined` value, running `onDefined` when the
38 * value is present or evaluating `onUndefined` when the value is `undefined`.
39 *
40 * **When to use**
41 *
42 * Use when you need to turn an `A | undefined` into a non-optional result by
43 * handling both the defined and undefined branches in one expression.
44 *
45 * @see {@link map} for transforming defined values while preserving `undefined`
46 * @see {@link getOrThrowWith} for throwing when the value is `undefined` instead of returning a fallback branch
47 *
48 * @category pattern matching
49 * @since 4.0.0
50 */
51export const match: {
52 <B, A, C = B>(options: {
53 readonly onUndefined: LazyArg<B>
54 readonly onDefined: (a: A) => C
55 }): (self: A | undefined) => B | C
56 <A, B, C = B>(self: A | undefined, options: {
57 readonly onUndefined: LazyArg<B>
58 readonly onDefined: (a: A) => C
59 }): B | C
60} = dual(
61 2,
62 <A, B, C = B>(self: A | undefined, { onDefined, onUndefined }: {
63 readonly onUndefined: LazyArg<B>
64 readonly onDefined: (a: A) => C
65 }): B | C => self === undefined ? onUndefined() : onDefined(self)
66)
67
68/**
69 * Returns the defined value, or throws the value produced by `onUndefined`
70 * when the input is `undefined`.
71 *
72 * **When to use**
73 *
74 * Use when you need fail-fast unwrapping of an `A | undefined` value and want
75 * to provide the thrown error for the undefined case.
76 *
77 * **Details**
78 *
79 * Defined values are returned unchanged. When the input is `undefined`,
80 * `onUndefined` is called and its result is thrown.
81 *
82 * @see {@link getOrThrow} for the default-error sibling
83 * @see {@link match} for handling defined and undefined cases without throwing
84 *
85 * @category getters
86 * @since 4.0.0
87 */
88export const getOrThrowWith: {
89 (onUndefined: () => unknown): <A>(self: A | undefined) => A
90 <A>(self: A | undefined, onUndefined: () => unknown): A
91} = dual(2, <A>(self: A | undefined, onUndefined: () => unknown): A => {
92 if (self !== undefined) {
93 return self
94 }
95 throw onUndefined()
96})
97
98/**
99 * Returns the defined value, or throws a default `Error` when the input is
100 * `undefined`.
101 *
102 * **When to use**
103 *
104 * Use when you need to unwrap a value that should already be defined and a
105 * generic missing-value `Error` is acceptable.
106 *
107 * **Details**
108 *
109 * Defined inputs are returned unchanged. `undefined` throws
110 * `new Error("getOrThrow called on a undefined")`.
111 *
112 * @see {@link getOrThrowWith} for the sibling that lets callers choose the thrown value
113 * @see {@link match} for handling defined and undefined cases without throwing
114 *
115 * @category getters
116 * @since 4.0.0
117 */
118export const getOrThrow: <A>(self: A | undefined) => A = getOrThrowWith(() =>
119 new Error("getOrThrow called on a undefined")
120)
121
122/**
123 * Converts a throwing function into one that returns successful results
124 * unchanged and returns `undefined` when the function throws.
125 *
126 * **When to use**
127 *
128 * Use to adapt exception-throwing functions when `undefined` is the absence
129 * value you want to return for failures.
130 *
131 * **Gotchas**
132 *
133 * Thrown values are discarded. If the wrapped function can successfully return
134 * `undefined`, that success is indistinguishable from a thrown failure.
135 *
136 * @category converting
137 * @since 4.0.0
138 */
139export const liftThrowable = <A extends ReadonlyArray<unknown>, B>(
140 f: (...a: A) => B
141): (...a: A) => B | undefined =>
142(...a) => {
143 try {
144 return f(...a)
145 } catch {
146 return undefined
147 }
148}
149
150/**
151 * Creates a `Reducer` for `UndefinedOr<A>` that prioritizes the first non-`undefined`
152 * value and combines values when both operands are present.
153 *
154 * **When to use**
155 *
156 * Use when you need to reduce values that may be `undefined`, keeping the
157 * first defined value as a fallback and combining only when both operands are
158 * defined.
159 *
160 * **Details**
161 *
162 * Combining `undefined` with `undefined` returns `undefined`. Combining a
163 * defined value with `undefined` keeps the defined value, so the first defined
164 * value wins when only one side is present. When both values are defined, they
165 * are combined with `combiner.combine`. The reducer's initial value is
166 * `undefined`.
167 *
168 * @category constructors
169 * @since 4.0.0
170 */
171export function makeReducer<A>(combiner: Combiner.Combiner<A>): Reducer.Reducer<A | undefined> {
172 return Reducer.make((self, that) => {
173 if (self === undefined) return that
174 if (that === undefined) return self
175 return combiner.combine(self, that)
176 }, undefined as A | undefined)
177}
178
179/**
180 * Creates a `Combiner` for `A | undefined` that combines values only when both
181 * operands are defined.
182 *
183 * **When to use**
184 *
185 * Use to lift a `Combiner` so any `undefined` operand makes the combined result
186 * `undefined`.
187 *
188 * **Details**
189 *
190 * - `undefined` combined with any value returns `undefined`
191 * - Any value combined with `undefined` returns `undefined`
192 * - `a` combined with `b` returns `combiner.combine(a, b)`
193 *
194 * @see {@link makeReducerFailFast} if you have a `Reducer` and want to lift it
195 * to `UndefinedOr` values.
196 *
197 * @category constructors
198 * @since 4.0.0
199 */
200export function makeCombinerFailFast<A>(combiner: Combiner.Combiner<A>): Combiner.Combiner<A | undefined> {
201 return Combiner.make((self, that) => {
202 if (self === undefined || that === undefined) return undefined
203 return combiner.combine(self, that)
204 })
205}
206
207/**
208 * Creates a `Reducer` for `A | undefined` by wrapping an existing reducer with
209 * fail-fast semantics.
210 *
211 * **When to use**
212 *
213 * Use to wrap an existing `Reducer` so any `undefined` value aborts the entire
214 * reduction result.
215 *
216 * **Details**
217 *
218 * - Initial value is the wrapped reducer's `initialValue`
219 * - Combining two defined values delegates to the wrapped reducer
220 * - If the accumulator or next value is `undefined`, the reduction returns `undefined`
221 *
222 * @see {@link makeCombinerFailFast} if you only have a `Combiner` and want to
223 * lift it to `UndefinedOr` values.
224 *
225 * @category constructors
226 * @since 4.0.0
227 */
228export function makeReducerFailFast<A>(reducer: Reducer.Reducer<A>): Reducer.Reducer<A | undefined> {
229 const combine = makeCombinerFailFast(reducer).combine
230 const initialValue = reducer.initialValue as A | undefined
231 return Reducer.make(combine, initialValue, (collection) => {
232 let out = initialValue
233 for (const value of collection) {
234 out = combine(out, value)
235 if (out === undefined) return out
236 }
237 return out
238 })
239}