/** * Represents the failure case of an Either * @typeParam E - The type of the error */ export type Left = {readonly _tag: 'Left', readonly left: E} /** * Represents the success case of an Either * @typeParam A - The type of the value */ export type Right = {readonly _tag: 'Right', readonly right: A} /** * A type that represents either a success (Right) or failure (Left) * @typeParam A - The success value type * @typeParam E - The error type */ export type Either = Right | Left /** * Constructs a Left value * @typeParam E - The error type * @param e - The error value * @returns A Left containing the error */ export const left = (e: E): Either => ({ _tag: 'Left', left: e, }) /** * Constructs a Right value * @typeParam A - The value type * @param a - The value * @returns A Right containing the value */ export const right = (a: A): Either => ({ _tag: 'Right', right: a, }) /** * Maps a function over the Right side of an Either * @typeParam A - The input value type * @typeParam B - The output value type * @typeParam E - The error type * @param f - The mapping function * @returns A function that takes an Either and returns a new Either */ export const eitherMap = (f: (a: A) => B) => (fa: Either): Either => fa._tag === 'Right' ? right(f(fa.right)) : fa /** * Chains computations that may return Either values * @typeParam A - The input value type * @typeParam B - The output value type * @typeParam E - The error type * @param f - The function that returns an Either * @returns A function that takes an Either and returns a new Either */ export const eitherChain = (f: (a: A) => Either) => (fa: Either): Either => fa._tag === 'Right' ? f(fa.right) : fa /** * Folds an Either into a single value by providing handlers for both cases * @typeParam A - The success value type * @typeParam E - The error type * @typeParam B - The output type * @param onLeft - Function to handle Left case * @param onRight - Function to handle Right case * @returns A function that takes an Either and returns the folded value */ export const fold = (onLeft: (e: E) => B, onRight: (a: A) => B) => (fa: Either): B => fa._tag === 'Left' ? onLeft(fa.left) : onRight(fa.right) /** * A class for working with Either in a do-notation style * @typeParam T - The type of the accumulated scope * @typeParam E - The error type */ export class DoEither, E> { /** * @param either - The current Either value */ constructor(private readonly either: Either) {} /** * Binds a new value to the scope * @typeParam K - The key to bind to * @typeParam A - The type of the value to bind * @param key - The property name to bind to * @param fa - The Either value to bind * @returns A new DoEither instance with the extended scope */ bind( key: K, fa: Either, ): DoEither, E> { const newEither = eitherChain, E>((scope: T) => eitherMap, E>((a: A) => ({...scope, [key]: a}))(fa) )(this.either) return new DoEither(newEither) } /** * Binds a new value to the scope using a lazily evaluated Either * @typeParam K - The key to bind to * @typeParam A - The type of the value to bind * @param key - The property name to bind to * @param f - Function returning the Either value to bind (receives current scope) * @returns A new DoEither instance with the extended scope */ bindL( key: K, f: (scope: T) => Either, ): DoEither, E> { const newEither = eitherChain, E>((scope: T) => eitherMap, E>((a: A) => ({...scope, [key]: a}))( f(scope), ) )(this.either) return new DoEither(newEither) } /** * Returns the final result from the scope * @typeParam B - The return type * @param f - The function to transform the scope * @returns An Either containing the result */ return(f: (scope: T) => B): Either { return eitherMap(f)(this.either) } /** * Returns the current scope * @returns The current Either scope */ done(): Either { return this.either } /** * Executes an effect without modifying the scope * @param fa - The Either effect to execute * @returns A new DoEither instance with the same scope */ do(fa: Either): DoEither { const newEither = eitherChain((scope: T) => eitherMap(() => scope)(fa) )(this.either) return new DoEither(newEither) } /** * Executes an effect that depends on the current scope * @param f - The function that returns an Either effect * @returns A new DoEither instance with the same scope */ doL(f: (scope: T) => Either): DoEither { const newEither = eitherChain((scope: T) => eitherMap(() => scope)(f(scope)) )(this.either) return new DoEither(newEither) } /** * Starts a new DoEither chain with an empty scope * @returns A new DoEither instance with an empty scope */ static start(): DoEither<{}, E> { return new DoEither(right({})) } } /** * A class for working with Either in a do-notation style with asynchronous computations * @typeParam T - The type of the accumulated scope * @typeParam E - The error type */ export class AsyncDoEither, E> { /** * @param promise - The current Promise of an Either value */ constructor(private readonly promise: Promise>) {} /** * Binds a new value to the scope synchronously * @typeParam K - The key to bind to * @typeParam A - The type of the value to bind * @param key - The property name to bind to * @param fa - The Either value to bind * @returns A new AsyncDoEither instance with the extended scope */ bind( key: K, fa: Either, ): AsyncDoEither, E> { return this.bindL(key, () => fa) } /** * Binds a new value to the scope asynchronously * @typeParam K - The key to bind to * @typeParam A - The type of the value to bind * @param key - The property name to bind to * @param f - Function returning Either or Promise of Either * @returns A new AsyncDoEither instance with the extended scope */ bindL( key: K, f: (scope: T) => Either | Promise>, ): AsyncDoEither, E> { const newPromise = this.promise.then(async (currentEither) => { if (isLeft(currentEither)) { return currentEither as unknown as Either, E> } const scope = currentEither.right try { const nextEither = await f(scope) if (isLeft(nextEither)) { return nextEither as unknown as Either, E> } return right({ ...scope, [key]: nextEither.right, }) as Either, E> } catch (err) { return left(err as E) as Either, E> } }) return new AsyncDoEither(newPromise) } /** * Returns the final result from the scope * @typeParam B - The return type * @param f - The function to transform the scope * @returns A Promise of Either containing the result */ return(f: (scope: T) => B): Promise> { return this.promise.then(either => { if (isLeft(either)) { return either as unknown as Either } return right(f(either.right)) }) } /** * Returns the current scope * @returns A Promise of the current Either scope */ done(): Promise> { return this.promise } /** * Executes an effect without modifying the scope * @param fa - The Either effect to execute * @returns A new AsyncDoEither instance with the same scope */ do(fa: Either): AsyncDoEither { return this.doL(() => fa) } /** * Executes an effect that depends on the current scope * @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 { const newPromise = this.promise.then(async (currentEither) => { if (isLeft(currentEither)) { return currentEither } const scope = currentEither.right try { const effectEither = await f(scope) if (isLeft(effectEither)) { return effectEither as unknown as Either } return currentEither } catch (err) { return left(err as E) } }) return new AsyncDoEither(newPromise) } /** * Starts a new AsyncDoEither chain with an empty scope * @returns A new AsyncDoEither instance with an empty scope */ static start(): AsyncDoEither<{}, E> { return new AsyncDoEither(Promise.resolve(right({}))) } }