All files utils.ts

100% Statements 12/12
100% Branches 3/3
100% Functions 2/2
100% Lines 11/11

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234                                                                                                                                                                                                                                                                                                                                                              13x 13x   13x 25x 6x   19x 19x 7x         13x                                                                               14x     1x  
// utils.ts
import { MaybePromise } from './types'
 
/**
 * Helper type to compute the output type of a pipeline of functions.
 * Recursively determines the type of the result after applying each function in sequence.
 * @template T The current input type.
 * @template Fs The array of functions in the pipeline.
 * @internal
 */
type PipeChain<T, Fs extends Array<(arg: any) => MaybePromise<any>>> = Fs extends [infer First, ...infer Rest]
  ? First extends (arg: T) => MaybePromise<infer U>
    ? Rest extends Array<(arg: any) => MaybePromise<any>>
      ? PipeChain<U, Rest>
      : U
    : never
  : T
 
/**
 * Helper type to determine if any function in the pipeline returns a Promise.
 * Checks each function's return type to identify asynchronous operations.
 * @template Fs The array of functions.
 * @internal
 */
type HasPromise<Fs extends Array<(arg: any) => any>> = Fs extends [infer First, ...infer Rest]
  ? First extends (arg: any) => infer U
    ? U extends Promise<any>
      ? true
      : Rest extends Array<(arg: any) => any>
      ? HasPromise<Rest>
      : false
    : false
  : false
 
/**
 * Helper type to determine the return type of the pipe function.
 * Returns a Promise if any function is asynchronous, otherwise returns the synchronous type.
 * @template T The initial value type.
 * @template Fs The array of functions.
 * @internal
 */
type PipeReturn<T, Fs extends Array<(arg: any) => any>> = HasPromise<Fs> extends true
  ? Promise<PipeChain<T, Fs>>
  : PipeChain<T, Fs>
 
/**
 * Composes functions in a pipeline, handling both synchronous and asynchronous operations.
 * Applies a series of functions to an initial value, passing the result of each function to the next.
 * Returns a synchronous value if all functions are synchronous, or a Promise if any function returns a Promise.
 * @template T The type of the initial value.
 * @template Fs The tuple of functions to apply in sequence.
 * @param value The initial value to start the pipeline.
 * @param fns A tuple of functions to apply in sequence. Each function takes the previous result and returns a new value or Promise.
 * @returns The final result of the pipeline, typed as a synchronous value or Promise based on the functions.
 * @example
 * ```typescript
 * import { pipe } from 'neverever';
 *
 * // Example 1: Synchronous pipeline with numbers
 * const syncResult = pipe(
 *   5,
 *   (n: number) => n * 2,
 *   (n: number) => n + 10
 * );
 * console.log(syncResult); // 20
 *
 * // Example 2: Asynchronous pipeline with strings
 * const asyncResult = pipe(
 *   'hello',
 *   async (s: string) => s.toUpperCase(),
 *   (s: string) => s + '!'
 * );
 * console.log(await asyncResult); // 'HELLO!'
 *
 * // Example 3: Mixed synchronous and asynchronous pipeline
 * const mixedResult = pipe(
 *   42,
 *   (n: number) => n * 2,
 *   async (n: number) => n + 10
 * );
 * console.log(await mixedResult); // 94
 *
 * // Example 4: Pipeline with Option
 * import { some, none } from 'neverever';
 * const optionResult = pipe(
 *   some(42),
 *   (opt: Option<number>) => opt.map(n => n * 2),
 *   (opt: Option<number>) => opt.map(n => n + 10)
 * );
 * console.log(optionResult.unwrapOr(0)); // 94
 *
 * // Example 5: Pipeline with Option returning none
 * const noneResult = pipe(
 *   none() as Option<number>,
 *   (opt: Option<number>) => opt.map(n => n * 2),
 *   (opt: Option<number>) => opt.map(n => n + 10)
 * );
 * console.log(noneResult.unwrapOr(0)); // 0
 *
 * // Example 6: Pipeline with Result
 * import { ok, err } from 'neverever';
 * const resultPipeline = pipe(
 *   ok<string, string>('data'),
 *   (res: Result<string, string>) => res.map(s => s.toUpperCase()),
 *   (res: Result<string, string>) => res.map(s => s + '!')
 * );
 * console.log(resultPipeline.unwrapOr('')); // 'DATA!'
 *
 * // Example 7: Pipeline with ResultAsync
 * const resultAsyncPipeline = pipe(
 *   ok<string, string>('data').toAsync(),
 *   async (res: ResultLike<string, string>) => {
 *     const resolved = res instanceof Promise ? await res : res;
 *     return resolved.map(s => s.toUpperCase());
 *   },
 *   async (res: ResultLike<string, string>) => {
 *     const resolved = res instanceof Promise ? await res : res;
 *     return resolved.map(s => s + '!');
 *   }
 * );
 * console.log(await (await resultAsyncPipeline).unwrapOr('')); // 'DATA!'
 *
 * // Example 8: Pipeline with Promise<Result>
 * const promiseResultPipeline = pipe(
 *   Promise.resolve(ok<string, string>('data')),
 *   async (res: ResultLike<string, string>) => {
 *     const resolved = res instanceof Promise ? await res : res;
 *     return resolved.map(s => s.toUpperCase());
 *   },
 *   async (res: ResultLike<string, string>) => {
 *     const resolved = res instanceof Promise ? await res : res;
 *     return resolved.map(s => s + '!');
 *   }
 * );
 * console.log(await (await promiseResultPipeline).unwrapOr('')); // 'DATA!'
 *
 * // Example 9: Pipeline with OptionAsync
 * const optionAsyncResult = pipe(
 *   some(42).toAsync(),
 *   async (opt: OptionLike<number>) => {
 *     const resolved = opt instanceof Promise ? await opt : opt;
 *     const asyncOpt = 'toAsync' in resolved ? resolved.toAsync() : resolved;
 *     return asyncOpt.map(n => n * 2);
 *   },
 *   async (opt: OptionLike<number>) => {
 *     const resolved = opt instanceof Promise ? await opt : opt;
 *     const asyncOpt = 'toAsync' in resolved ? resolved.toAsync() : resolved;
 *     return asyncOpt.map(n => n + 10);
 *   }
 * );
 * console.log(await (await optionAsyncResult).unwrapOr(0)); // 94
 *
 * // Example 10: Empty pipeline
 * const emptyResult = pipe(42);
 * console.log(emptyResult); // 42
 *
 * // Example 11: Error propagation in asynchronous pipeline
 * const errorResult = pipe(
 *   'test',
 *   async (s: string) => { throw new Error('fail'); },
 *   (s: string) => s + '!'
 * );
 * await errorResult.catch(e => console.log(e.message)); // 'fail'
 *
 * // Example 12: Complex pipeline with multiple transformations
 * const complexResult = pipe(
 *   { value: 10 },
 *   (obj: { value: number }) => ({ value: obj.value * 2 }),
 *   async (obj: { value: number }) => ({ value: obj.value + 5 }),
 *   (obj: { value: number }) => obj.value.toString()
 * );
 * console.log(await complexResult); // '25'
 * ```
 */
function pipe<T, Fs extends Array<(arg: any) => any>>(value: T, ...fns: Fs): PipeReturn<T, Fs> {
  let result: any = value
  let isAsync = false
 
  for (const fn of fns) {
    if (isAsync) {
      result = Promise.resolve(result).then(fn)
    } else {
      result = fn(result)
      if (result instanceof Promise) {
        isAsync = true
      }
    }
  }
 
  return result as PipeReturn<T, Fs>
}
 
/**
 * Unwraps a MaybePromise to its inner value, ensuring a Promise is returned for consistent handling.
 * If the input is a synchronous value, it is wrapped in a resolved Promise.
 * If the input is already a Promise, it is returned as-is.
 * @template T The inner type of the MaybePromise.
 * @param value A MaybePromise value, which can be a synchronous value or a Promise.
 * @returns A Promise resolving to the inner value of the MaybePromise.
 * @example
 * ```typescript
 * import { unwrapMaybePromise } from 'neverever';
 * import { some } from 'neverever';
 *
 * // Example 1: Unwrapping a synchronous value
 * const syncValue: number = 42;
 * const syncResult = unwrapMaybePromise(syncValue);
 * console.log(await syncResult); // 42
 *
 * // Example 2: Unwrapping a Promise
 * const asyncValue: Promise<string> = Promise.resolve('hello');
 * const asyncResult = unwrapMaybePromise(asyncValue);
 * console.log(await asyncResult); // 'hello'
 *
 * // Example 3: Unwrapping a MaybePromise containing an Option
 * const optionValue: MaybePromise<Option<number>> = some(42);
 * const optionResult = unwrapMaybePromise(optionValue);
 * console.log((await optionResult).unwrapOr(0)); // 42
 *
 * // Example 4: Handling null or undefined
 * const nullValue: MaybePromise<null> = null;
 * console.log(await unwrapMaybePromise(nullValue)); // null
 *
 * // Example 5: Handling a rejected Promise
 * const rejectedValue: MaybePromise<string> = Promise.reject(new Error('fail'));
 * unwrapMaybePromise(rejectedValue).catch(e => console.log(e.message)); // 'fail'
 * ```
 */
function unwrapMaybePromise<T>(value: MaybePromise<T>): Promise<T> {
  return Promise.resolve(value)
}
 
export { pipe, unwrapMaybePromise }