diff --git a/src/either.ts b/src/either.ts index fcba02e..67d0355 100644 --- a/src/either.ts +++ b/src/either.ts @@ -1,3 +1,5 @@ +import { Option } from './option' + /** * Represents the failure case of an Either * @typeParam E - The type of the error @@ -77,6 +79,50 @@ export const fold = (fa: Either): B => fa._tag === 'Left' ? onLeft(fa.left) : onRight(fa.right) +/** + * Converts an Option to an Either, providing a default error for None. + * @typeParam A - Value type + * @typeParam E - Error type + * @param onNone - Error value for None case + * @returns Function that converts Option to Either + */ +export const fromOption = + (onNone: E) => (option: Option): Either => + option._tag === 'Some' ? right(option.value) : left(onNone) + +/** + * Lazily converts an Option to an Either (error evaluated only when needed). + * @typeParam A - Value type + * @typeParam E - Error type + * @param onNone - Function returning error for None case + * @returns Function that converts Option to Either + */ +export const fromOptionL = + (onNone: () => E) => (option: Option): Either => + option._tag === 'Some' ? right(option.value) : left(onNone()) + +/** + * Converts a Promise(p: Promise>): Promise> => + p.then(option => fromOption(onNone)(option)) + +/** + * Lazily converts a Promise(p: Promise>): Promise> => + p.then(option => fromOptionL(onNone)(option)) + /** * A class for working with Either in a do-notation style * @typeParam T - The type of the accumulated scope @@ -277,10 +323,9 @@ export class AsyncDoEither, E> { * @param f - Function returning Either or Promise of Either * @returns A new AsyncDoEither instance with the same scope */ - doL(f: - (scope: T) => - | Either - | Promise>): AsyncDoEither + doL(f: (scope: T) => + | Either + | Promise>): AsyncDoEither { const newPromise = this.promise.then(async (currentEither) => { if (currentEither._tag === 'Left') { diff --git a/src/option.ts b/src/option.ts index 84d678a..12f6836 100644 --- a/src/option.ts +++ b/src/option.ts @@ -1,3 +1,5 @@ +import { Either, left, right } from './either' + /** * Represents the absence of a value */ @@ -52,6 +54,54 @@ export const chain = (f: (a: A) => Option) => (fa: Option): Option => fa._tag === 'Some' ? f(fa.value) : none +/** + * Folds an Option into a single value by providing handlers for both cases (Some/None) + * @typeParam A - The value type contained in the Option + * @typeParam B - The output type of the fold operation + * @param onNone - Function to handle the None case + * @param onSome - Function to handle the Some case + * @returns A function that takes an Option and returns the folded value + * @example + * const result = fold( + * () => 'No value', + * (value: number) => `Value: ${value}` + * )(some(42)) // Returns "Value: 42" + * + * @example + * const result = fold( + * () => 'No value', + * (value: number) => `Value: ${value}` + * )(none) // Returns "No value" + */ +export const fold = + (onNone: () => B, onSome: (value: A) => B) => (option: Option): B => + option._tag === 'Some' ? onSome(option.value) : onNone() + +/** + * A curried version of fold that takes the Option first + * @typeParam A - The value type contained in the Option + * @typeParam B - The output type of the fold operation + * @param option - The Option to fold + * @returns An object with methods to handle both cases + * @example + * some(42).fold({ + * onNone: () => 'No value', + * onSome: (value) => `Value: ${value}` + * }) // Returns "Value: 42" + */ +export const foldC = (option: Option) => ({ + /** + * @param handlers - Object containing both case handlers + * @param handlers.onNone - Function to handle None case + * @param handlers.onSome - Function to handle Some case + * @returns The folded value + */ + fold: (handlers: {onNone: () => B, onSome: (value: A) => B}): B => + option._tag === 'Some' + ? handlers.onSome(option.value) + : handlers.onNone(), +}) + /** * Extracts the value from an Option or returns a default * @typeParam A - The value type @@ -155,6 +205,42 @@ export class DoOption> { return this.option } + /** + * Converts the DoOption to an Either, providing a default error value for the None case + * @typeParam E - The type of the error to use if the Option is None + * @param onNone - The error value to use if the Option is None + * @returns An Either - Right with the accumulated scope if the Option is Some, + * or Left with the provided error if the Option is None + * @example + * DoOption.start() + * .bind('id', some(1)) + * .toEither('Missing value') + * // Returns Right({ id: 1 }) or Left('Missing value') + */ + toEither(onNone: E): Either { + return this.option._tag === 'Some' + ? right(this.option.value) + : left(onNone) + } + + /** + * Converts the DoOption to an Either, lazily providing an error value for the None case + * @typeParam E - The type of the error to use if the Option is None + * @param onNone - A function that returns the error value when Option is None + * @returns An Either - Right with the accumulated scope if the Option is Some, + * or Left with the computed error if the Option is None + * @example + * DoOption.start() + * .bind('config', loadConfig()) + * .toEitherL(() => new Error('Config missing')) + * // Returns Right(config) or Left(Error) + */ + toEitherL(onNone: () => E): Either { + return this.option._tag === 'Some' + ? right(this.option.value) + : left(onNone()) + } + /** * Executes an effect without modifying the scope * @param fa - The Option effect to execute (does not depend on scope) @@ -303,6 +389,46 @@ export class AsyncDoOption> { ) } + /** + * Converts the AsyncDoOption to a Promise of Either, providing a default error value for the None case + * @typeParam E - The type of the error to use if the Option is None + * @param onNone - The error value to use if the Option is None + * @returns A Promise that resolves to an Either - Right with the accumulated scope if the Option is Some, + * or Left with the provided error if the Option is None + * @example + * AsyncDoOption.start() + * .bind('user', getUserAsync()) + * .toEither('User not found') + * .then(either => { ... }) // either is Either<{ user: User }, string> + */ + toEither(onNone: E): Promise> { + return this.promise.then(option => + option._tag === 'Some' + ? right(option.value) + : left(onNone) + ) + } + + /** + * Converts the AsyncDoOption to a Promise of Either, lazily providing an error value for the None case + * @typeParam E - The type of the error to use if the Option is None + * @param onNone - A function that returns the error value to use if the Option is None + * @returns A Promise that resolves to an Either - Right with the accumulated scope if the Option is Some, + * or Left with the error from onNone if the Option is None + * @example + * AsyncDoOption.start() + * .bind('data', fetchData()) + * .toEitherL(() => new Error('Data fetch failed')) + * .then(either => { ... }) // either is Either<{ data: Data }, Error> + */ + toEitherL(onNone: () => E): Promise> { + return this.promise.then(option => + option._tag === 'Some' + ? right(option.value) + : left(onNone()) + ) + } + /** * Returns the current scope * @returns A promise of the current Option scope