Skip to content

Commit e7837e2

Browse files
feat(result): add new methods for enhanced error handling and transformations (#207)
Introduced several new instance methods to the IResult interface: recover, recoverWith, orElse, swap, zipWith, flatMapPromise, and flatMapObservable. These provide advanced error handling and transformation capabilities, allowing for more flexible recovery mechanisms and better integration with asynchronous and reactive programming models. Added static methods to Result for converting promises and observables into Results: fromPromise and fromObservable, along with their respective transformer functions promiseToResult and observableToResult. This facilitates seamless conversion between different asynchronous paradigms and the Result monadic structure. Accompanied by comprehensive tests covering all new functionalities to ensure correctness and reliability in various scenarios. These enhancements enable improved functional error handling patterns consistent with modern TypeScript development practices.
1 parent d38a0aa commit e7837e2

13 files changed

+1779
-30
lines changed

src/maybe/maybe.ts

+5-6
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,11 @@ export class Maybe<T> implements IMaybe<T> {
2929
*
3030
* @example
3131
* // Convert a promise to a Maybe
32-
* const userMaybe = await Maybe.fromPromise(api.fetchUser(userId));
33-
*
34-
* userMaybe.match({
35-
* some: user => console.log(user.name),
36-
* none: () => console.log('User not found')
37-
* });
32+
* Maybe.fromPromise(api.fetchUser(userId))
33+
* .then(userMaybe => userMaybe.match({
34+
* some: user => console.log(user.name),
35+
* none: () => console.log('User not found')
36+
* }));
3837
*/
3938
/**
4039
* Creates a Maybe from a Promise.

src/maybe/transformers/try-promise-to-maybe.ts

+21-24
Original file line numberDiff line numberDiff line change
@@ -2,41 +2,38 @@ import { IMaybe } from '../maybe.interface'
22
import { Maybe } from '../maybe'
33

44
/**
5-
* Wraps an async function to catch errors and return a Maybe.
5+
* Wraps a function that returns a Promise to catch errors and return a Maybe.
66
*
7-
* Executes the provided function within a try/catch block and converts the result:
8-
* - If the function succeeds, returns a Some containing the result
9-
* - If the function throws an error, returns None
7+
* Executes the provided function and converts the result:
8+
* - If the function's Promise resolves, returns a Some containing the result
9+
* - If the function's Promise rejects, returns None
1010
*
1111
* This is particularly useful for handling API calls or other operations that may fail,
1212
* allowing for more functional error handling without explicit try/catch blocks.
1313
*
14-
* @param fn The function to execute
14+
* @param fn The function that returns a Promise
1515
* @returns A Promise that resolves to a Maybe containing the result if successful
1616
*
1717
* @example
1818
* // Without using tryPromiseToMaybe
19-
* try {
20-
* const data = await api.fetchUserData(userId);
21-
* return maybe(data);
22-
* } catch (error) {
23-
* return none();
19+
* function fetchUserData(userId) {
20+
* return api.fetchUserData(userId)
21+
* .then(data => maybe(data))
22+
* .catch(() => none());
2423
* }
2524
*
2625
* // Using tryPromiseToMaybe
27-
* const userDataMaybe = await tryPromiseToMaybe(() => api.fetchUserData(userId));
28-
*
29-
* // Then use the Maybe as normal
30-
* userDataMaybe.match({
31-
* some: data => displayUserData(data),
32-
* none: () => showErrorMessage('Could not load user data')
33-
* });
26+
* tryPromiseToMaybe(() => api.fetchUserData(userId))
27+
* .then(userDataMaybe => {
28+
* // Then use the Maybe as normal
29+
* userDataMaybe.match({
30+
* some: data => displayUserData(data),
31+
* none: () => showErrorMessage('Could not load user data')
32+
* });
33+
* });
3434
*/
35-
export async function tryPromiseToMaybe<T>(fn: () => Promise<T>): Promise<IMaybe<NonNullable<T>>> {
36-
try {
37-
const result = await fn()
38-
return new Maybe<NonNullable<T>>(result as NonNullable<T>)
39-
} catch (error) {
40-
return new Maybe<NonNullable<T>>()
41-
}
35+
export function tryPromiseToMaybe<T>(fn: () => Promise<T>): Promise<IMaybe<NonNullable<T>>> {
36+
return fn()
37+
.then(result => new Maybe<NonNullable<T>>(result as NonNullable<T>))
38+
.catch(() => new Maybe<NonNullable<T>>())
4239
}

src/result/public_api.ts

+3
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,6 @@ export * from './transformers/result-to-promise'
55
export * from './transformers/try-catch-to-result'
66
export * from './transformers/unwrap-result'
77
export * from './transformers/result-to-observable'
8+
export * from './transformers/promise-to-result'
9+
export * from './transformers/try-promise-to-result'
10+
export * from './transformers/observable-to-result'

src/result/result.interface.ts

+281
Original file line numberDiff line numberDiff line change
@@ -759,6 +759,287 @@ export interface IResult<T, E> {
759759
* });
760760
*/
761761
tapFailThru(fn: (val: E) => void): IResult<T, E>
762+
763+
/**
764+
* Transforms a Fail Result into an Ok Result using a recovery function.
765+
*
766+
* This method provides error handling by:
767+
* - If this Result is a Fail variant, it applies the function to the error to generate a recovery value
768+
* and returns a new Ok Result with that value
769+
* - If this Result is an Ok variant, it returns the original Result unchanged
770+
*
771+
* This is similar to a catch block in try/catch, but in a functional style.
772+
*
773+
* @param fn - A function that transforms the Fail value into a recovery value
774+
* @returns An Ok Result with either the original value or the recovered value
775+
*
776+
* @example
777+
* // Providing default values
778+
* const userResult = getUserById(userId)
779+
* .recover(error => ({
780+
* id: 0,
781+
* name: "Guest User",
782+
* isGuest: true
783+
* }));
784+
*
785+
* // userResult is guaranteed to be Ok
786+
* const user = userResult.unwrap(); // Safe, will never throw
787+
*
788+
* // Error logging with recovery
789+
* fetchData()
790+
* .tapFail(error => logError(error))
791+
* .recover(error => {
792+
* const fallbackData = getLocalData();
793+
* trackRecovery("data_fetch", error);
794+
* return fallbackData;
795+
* })
796+
* .map(data => processData(data)); // This will always run with either fetched or fallback data
797+
*/
798+
recover(fn: (err: E) => T): IResult<T, E>
799+
800+
/**
801+
* Transforms a Fail Result by applying a function that returns another Result.
802+
*
803+
* This method provides advanced error recovery by:
804+
* - If this Result is a Fail variant, it applies the function to the error, which returns a new Result
805+
* - If this Result is an Ok variant, it returns the original Result unchanged
806+
*
807+
* This is useful for fallback operations that might themselves fail.
808+
*
809+
* @param fn - A function that takes the Fail value and returns a new Result
810+
* @returns The original Result if Ok, or the Result returned by the function if Fail
811+
*
812+
* @example
813+
* // Trying a fallback operation that might also fail
814+
* fetchFromPrimaryAPI()
815+
* .recoverWith(error => {
816+
* logFailure(error, "primary_api");
817+
* // Try the backup API, which might also fail
818+
* return fetchFromBackupAPI();
819+
* })
820+
* .match({
821+
* ok: data => renderData(data),
822+
* fail: error => showFatalError("All data sources failed")
823+
* });
824+
*
825+
* // Authentication with multiple strategies
826+
* authenticateWithPassword(credentials)
827+
* .recoverWith(error => {
828+
* if (error.code === "CREDENTIALS_EXPIRED") {
829+
* return authenticateWithToken(refreshToken);
830+
* }
831+
* return fail(error); // Pass through other errors
832+
* })
833+
* .recoverWith(error => {
834+
* if (error.code === "TOKEN_EXPIRED") {
835+
* return authenticateWithOAuth();
836+
* }
837+
* return fail(error); // Pass through other errors
838+
* });
839+
*/
840+
recoverWith(fn: (err: E) => IResult<T, E>): IResult<T, E>
841+
842+
/**
843+
* Returns this Result if it's Ok, otherwise returns the provided fallback Result.
844+
*
845+
* This method allows for specifying an alternative Result:
846+
* - If this Result is an Ok variant, it is returned unchanged
847+
* - If this Result is a Fail variant, the fallback Result is returned
848+
*
849+
* @param fallback - The Result to return if this Result is Fail
850+
* @returns This Result if Ok, or the fallback Result if Fail
851+
*
852+
* @example
853+
* // Try multiple data sources in order
854+
* const userData = getUserFromCache(userId)
855+
* .orElse(getUserFromDatabase(userId))
856+
* .orElse(getUserFromBackupService(userId));
857+
*
858+
* // Providing a default value as a Result
859+
* const config = loadConfig()
860+
* .orElse(ok(DEFAULT_CONFIG));
861+
*
862+
* // config is guaranteed to be Ok
863+
* const configValue = config.unwrap(); // Safe, will never throw
864+
*/
865+
orElse(fallback: IResult<T, E>): IResult<T, E>
866+
867+
/**
868+
* Swaps the Ok and Fail values, creating a new Result with inversed variants.
869+
*
870+
* This method transforms:
871+
* - An Ok Result into a Fail Result with the same value (now as an error)
872+
* - A Fail Result into an Ok Result with the same error (now as a value)
873+
*
874+
* This can be useful for inverting logic or for protocols where the error case
875+
* is actually the expected or desired outcome.
876+
*
877+
* @returns A new Result with the Ok and Fail variants swapped
878+
*
879+
* @example
880+
* // Inverting validation logic
881+
* const isInvalid = validateInput(input) // Returns Ok if valid, Fail if invalid
882+
* .swap() // Returns Fail if valid, Ok if invalid
883+
* .isOk(); // true if the input was invalid
884+
*
885+
* // Working with negative conditions
886+
* const userNotFound = findUser(userId)
887+
* .swap()
888+
* .isOk(); // true if the user was not found
889+
*
890+
* // Converting between error domains
891+
* checkPermission(user, resource) // Returns Ok(true) if permitted, Fail(error) if not
892+
* .swap() // Returns Fail(true) if permitted, Ok(error) if not
893+
* .map(error => ({ // Only runs for permission errors
894+
* type: 'ACCESS_DENIED',
895+
* message: `Access denied: ${error.message}`,
896+
* resource
897+
* }))
898+
* .swap(); // Back to Ok for permitted, Fail for denied
899+
*/
900+
swap(): IResult<E, T>
901+
902+
/**
903+
* Combines this Result with another Result using a combining function.
904+
*
905+
* This method allows for working with two independent Results together:
906+
* - If both Results are Ok, applies the function to both values and returns an Ok Result
907+
* - If either Result is Fail, returns the first Fail Result encountered
908+
*
909+
* This is useful for combining data that comes from different sources where both
910+
* are needed to proceed.
911+
*
912+
* @param other - Another Result to combine with this one
913+
* @param fn - A function that combines the two Ok values
914+
* @returns A new Result containing either the combined values or the first error
915+
*
916+
* @example
917+
* // Combining user data and preferences that are loaded separately
918+
* const userData = fetchUserData(userId);
919+
* const userPrefs = fetchUserPreferences(userId);
920+
*
921+
* const userProfile = userData.zipWith(
922+
* userPrefs,
923+
* (data, prefs) => ({
924+
* ...data,
925+
* preferences: prefs,
926+
* theme: prefs.theme || 'default'
927+
* })
928+
* );
929+
*
930+
* // Working with multiple API responses
931+
* const orders = fetchOrders(userId);
932+
* const payments = fetchPayments(userId);
933+
*
934+
* const combinedData = orders.zipWith(
935+
* payments,
936+
* (orderList, paymentList) => {
937+
* return orderList.map(order => ({
938+
* ...order,
939+
* payments: paymentList.filter(p => p.orderId === order.id)
940+
* }));
941+
* }
942+
* );
943+
*/
944+
zipWith<U, R>(other: IResult<U, E>, fn: (a: T, b: U) => R): IResult<R, E>
945+
946+
/**
947+
* Maps the Ok value of this Result to a Promise, and then flattens the resulting structure.
948+
*
949+
* This method allows for seamless integration with asynchronous code:
950+
* - If this Result is an Ok variant, it applies the function to the contained value,
951+
* awaits the Promise, and wraps the resolved value in a new Ok Result
952+
* - If the Promise rejects, it returns a Fail Result with the rejection reason
953+
* - If this Result is a Fail variant, it returns a Promise that resolves to the
954+
* original Fail Result without calling the function
955+
*
956+
* @param fn - A function that takes the Ok value and returns a Promise
957+
* @returns A Promise that resolves to a new Result
958+
*
959+
* @example
960+
* // Chaining synchronous and asynchronous operations
961+
* validateUser(userData)
962+
* .flatMapPromise(user => saveUserToDatabase(user))
963+
* .then(result => result.match({
964+
* ok: savedUser => sendWelcomeEmail(savedUser),
965+
* fail: error => logError("Failed to save user", error)
966+
* }));
967+
*
968+
* // Multi-step asynchronous workflow
969+
* async function processOrder(orderData) {
970+
* // Start with sync validation returning a Result
971+
* const result = await validateOrder(orderData)
972+
* .flatMapPromise(async order => {
973+
* // Async payment processing
974+
* const paymentResult = await processPayment(order.paymentDetails);
975+
* if (!paymentResult.success) {
976+
* throw new Error(`Payment failed: ${paymentResult.message}`);
977+
* }
978+
*
979+
* // Async inventory check and allocation
980+
* await allocateInventory(order.items);
981+
*
982+
* // Return the processed order
983+
* return {
984+
* ...order,
985+
* status: 'PAID',
986+
* paymentId: paymentResult.id
987+
* };
988+
* })
989+
* .flatMapPromise(async paidOrder => {
990+
* // Final database save
991+
* const orderId = await saveOrderToDatabase(paidOrder);
992+
* return { ...paidOrder, id: orderId };
993+
* });
994+
*
995+
* return result;
996+
* }
997+
*/
998+
flatMapPromise<M>(fn: (val: T) => Promise<M>): Promise<IResult<M, E>>
999+
1000+
/**
1001+
* Maps the Ok value of this Result to an Observable, and then flattens the resulting structure.
1002+
*
1003+
* This method allows for seamless integration with reactive code:
1004+
* - If this Result is an Ok variant, it applies the function to the contained value,
1005+
* subscribes to the Observable, and wraps the first emitted value in a new Ok Result
1006+
* - If the Observable errors, it returns a Fail Result with the error
1007+
* - If the Observable completes without emitting, it returns a Fail Result with the provided default error
1008+
* - If this Result is a Fail variant, it returns a Promise that resolves to the
1009+
* original Fail Result without calling the function
1010+
*
1011+
* @param fn - A function that takes the Ok value and returns an Observable
1012+
* @param defaultError - The error to use if the Observable completes without emitting
1013+
* @returns A Promise that resolves to a new Result
1014+
*
1015+
* @requires rxjs@^7.0
1016+
* @example
1017+
* // Chaining Result with reactive code
1018+
* validateUser(userData)
1019+
* .flatMapObservable(
1020+
* user => userService.save(user),
1021+
* new Error("Failed to save user")
1022+
* )
1023+
* .then(result => result.match({
1024+
* ok: savedUser => notifyUserCreated(savedUser),
1025+
* fail: error => logError("User creation failed", error)
1026+
* }));
1027+
*
1028+
* // Processing real-time data
1029+
* getSensorData()
1030+
* .flatMapObservable(
1031+
* config => sensorApi.connectAndGetReading(config),
1032+
* new Error("No sensor reading received")
1033+
* )
1034+
* .then(result => result.match({
1035+
* ok: reading => updateDashboard(reading),
1036+
* fail: error => showSensorError(error)
1037+
* }));
1038+
*/
1039+
flatMapObservable<M>(
1040+
fn: (val: T) => import('rxjs').Observable<M>,
1041+
defaultError: E
1042+
): Promise<IResult<M, E>>
7621043
}
7631044

7641045
export interface IResultOk<T, E = never> extends IResult<T, E> {

0 commit comments

Comments
 (0)