Skip to content

Commit 2b2f534

Browse files
author
Erik
committed
Result.safe() supports an Error-class and unwraps nested Results. Result.map() now also allows callbacks that return a value directly.
1 parent 7f7b310 commit 2b2f534

File tree

3 files changed

+165
-51
lines changed

3 files changed

+165
-51
lines changed

README.md

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ function doStuff(value: number): Result<Error, number> {
131131

132132
#### Result.safe
133133

134-
Functions as a try-catch, returning the return-value of the callback on success, or the predefined error or caught error on failure:
134+
Functions as a try-catch, returning the return-value of the callback on success, or the predefined error(-class) or caught error on failure:
135135

136136
```ts
137137
// with caught error...
@@ -153,6 +153,17 @@ const result = Result.safe(new CustomError("Custom error!"), () => {
153153

154154
return value;
155155
}); // Result<CustomError, number>
156+
157+
// with predefined error-class...
158+
class CustomError extends Error {}
159+
160+
const result = Result.safe(CustomError, () => {
161+
let value = 2;
162+
163+
// code that might throw...
164+
165+
return value;
166+
}); // Result<CustomError, number>
156167
```
157168

158169
#### Result.combine
@@ -292,7 +303,7 @@ const value = result.getOrThrow();
292303
#### Result.map()
293304

294305
Maps a result to another result.
295-
If the result is success, it will call the callback-function with the encapsulated value, which must return another Result.
306+
If the result is success, it will call the callback-function with the encapsulated value, which returnr another Result.
296307
If the result is failure, it will ignore the callback-function, and will return the initial Result (error)
297308

298309
```ts
@@ -302,7 +313,13 @@ class ErrorB extends Error {}
302313
function doA(): Result<ErrorA, number> {}
303314
function doB(value: number): Result<ErrorB, string> {}
304315

305-
const result = doA().map(value => doB(value)); // Result<ErrorA | ErrorB, string>
316+
// nested results will flat-map to a single Result...
317+
const result1 = doA().map(value => doB(value)); // Result<ErrorA | ErrorB, string>
318+
319+
// ...or transform the successful value right away
320+
// note: underneath, the callback is wrapped inside Result.safe() in case the callback
321+
// might throw
322+
const result2 = doA().map(value => value * 2); // Result<ErrorA | Error, number>
306323
```
307324

308325
#### Result.forward()

src/index.ts

Lines changed: 89 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ function isAsyncFn(fn: Function) {
66
return fn.constructor.name === "AsyncFunction";
77
}
88

9+
function isResult(value: unknown): value is Result<any, any, any> {
10+
return value instanceof Ok || value instanceof Err;
11+
}
12+
913
interface SyncThenable {
1014
isSync: true;
1115
then<Fn extends () => Promise<any>>(cb: Fn): ReturnType<Fn>;
@@ -228,7 +232,8 @@ interface IResult<ErrorType, OkType> {
228232

229233
/**
230234
* **Maps a result to another result**
231-
* If the result is success, it will call the callback-function with the encapsulated value, which must return another Result.
235+
* If the result is success, it will call the callback-function with the encapsulated value, which returns another Result.
236+
* Nested Results are supported, which will basically act as a flat-map.
232237
* If the result is failure, it will ignore the callback-function.
233238
*
234239
* Example:
@@ -245,15 +250,29 @@ interface IResult<ErrorType, OkType> {
245250
* // ...
246251
* }
247252
*
248-
* const result = doA().map(value => doB(value)); // Result<ErrorA | ErrorB, string>
253+
* // nested results will flat-map to a single Result...
254+
* const result1 = doA().map(value => doB(value)); // Result<ErrorA | ErrorB, string>
255+
*
256+
* // ...or transform the successful value right away
257+
* // note: underneath, the callback is wrapped inside Result.safe() in case the callback
258+
* // might throw
259+
* const result2 = doA().map(value => value * 2); // Result<ErrorA | Error, number>
249260
* ```
250261
*/
251-
map<T extends Result<any, any, any>>(
252-
fn: (value: OkType) => T
253-
): JoinErrorTypes<ErrorType, T>;
254-
map<T extends Result<any, any, any>>(
262+
map<T>(
255263
fn: (value: OkType) => Promise<T>
256-
): Promise<JoinErrorTypes<ErrorType, T>>;
264+
): Promise<
265+
JoinErrorTypes<
266+
ErrorType,
267+
T extends Result<any, any, any> ? T : Result<Error, T, any>
268+
>
269+
>;
270+
map<T>(
271+
fn: (value: OkType) => T
272+
): JoinErrorTypes<
273+
ErrorType,
274+
T extends Result<any, any, any> ? T : Result<Error, T, any>
275+
>;
257276

258277
/**
259278
* **Rolls back things that were successful**
@@ -380,6 +399,10 @@ export namespace Result {
380399
return new Err<ErrorType, OkType, RollbackFn>(error, rollbackFn);
381400
}
382401

402+
type SafeReturnType<E, T> = T extends Result<any, any, any>
403+
? Result<E | InferErrorType<T>, InferOkType<T>, never>
404+
: Result<E, T, never>;
405+
383406
/**
384407
* **Functions as a try-catch, returning the return-value of the callback on success, or the predefined error or caught error on failure **
385408
*
@@ -404,39 +427,65 @@ export namespace Result {
404427
*
405428
* return value;
406429
* }); // Result<CustomError, number>
430+
*
431+
* // with predefined error Class...
432+
* const result = Result.safe(CustomError, () => {
433+
* let value = 2;
434+
*
435+
* // code that might throw...
436+
*
437+
* return value;
438+
* }); // Result<CustomError, number>
407439
* ```
408440
*/
409-
export function safe<ErrorType, OkType>(
410-
fn: () => Promise<OkType>
411-
): Promise<Result<Error, OkType, never>>;
412-
export function safe<ErrorType, OkType>(
413-
fn: () => OkType
414-
): Result<Error, OkType, never>;
415-
export function safe<ErrorType, OkType>(
416-
err: ErrorType,
417-
fn: () => Promise<OkType>
418-
): Promise<Result<ErrorType, OkType, never>>;
419-
export function safe<ErrorType, OkType>(
420-
err: ErrorType,
421-
fn: () => OkType
422-
): Result<ErrorType, OkType, never>;
441+
export function safe<T>(
442+
fn: () => Promise<T>
443+
): Promise<SafeReturnType<Error, T>>;
444+
export function safe<T>(fn: () => T): SafeReturnType<Error, T>;
445+
export function safe<ErrorType, T>(
446+
err: ErrorType | (new (...args: any[]) => ErrorType),
447+
fn: () => Promise<T>
448+
): Promise<SafeReturnType<ErrorType, T>>;
449+
export function safe<ErrorType, T>(
450+
err: ErrorType | (new (...args: any[]) => ErrorType),
451+
fn: () => T
452+
): SafeReturnType<ErrorType, T>;
423453
export function safe(errOrFn: any, fn?: any) {
424454
const hasCustomError = fn !== undefined;
425455

426456
const execute = hasCustomError ? fn : errOrFn;
427457

458+
function getError(caughtError: Error) {
459+
if (!hasCustomError) {
460+
// just forward the original Error
461+
return caughtError;
462+
}
463+
464+
// pass the caught error to the specified constructor
465+
if (typeof errOrFn === "function") {
466+
return new errOrFn(caughtError);
467+
}
468+
469+
// return predefined error
470+
return errOrFn;
471+
}
472+
428473
try {
429474
const resultOrPromise = execute();
430475

431476
if (resultOrPromise instanceof Promise) {
432477
return resultOrPromise
433-
.then(okValue => Result.ok(okValue))
434-
.catch(caughtError => error(hasCustomError ? errOrFn : caughtError));
478+
.then(okValue => {
479+
return isResult(okValue) ? okValue : Result.ok(okValue);
480+
})
481+
.catch(caughtError => error(getError(caughtError)));
435482
}
436483

437-
return ok(resultOrPromise);
484+
return isResult(resultOrPromise)
485+
? resultOrPromise
486+
: Result.ok(resultOrPromise);
438487
} catch (caughtError) {
439-
return error(hasCustomError ? errOrFn : caughtError);
488+
return error(getError(caughtError));
440489
}
441490
}
442491

@@ -650,20 +699,28 @@ abstract class Base<
650699
throw new Error("Method not implemented.");
651700
}
652701

653-
map<T extends Result<any, any, any>>(
654-
fn: (value: OkType) => T
655-
): JoinErrorTypes<ErrorType, T>;
656-
map<T extends Result<any, any, any>>(
702+
map<T>(
657703
fn: (value: OkType) => Promise<T>
658-
): Promise<JoinErrorTypes<ErrorType, T>>;
704+
): Promise<
705+
JoinErrorTypes<
706+
ErrorType,
707+
T extends Result<any, any, any> ? T : Result<Error, T, any>
708+
>
709+
>;
710+
map<T>(
711+
fn: (value: OkType) => T
712+
): JoinErrorTypes<
713+
ErrorType,
714+
T extends Result<any, any, any> ? T : Result<Error, T, any>
715+
>;
659716
map(fn: any) {
660717
if (this.isFailure()) {
661718
return isAsyncFn(fn) ? Promise.resolve(this) : this;
662719
}
663720

664-
const result = fn((this as any).value) as any;
721+
const result = Result.safe(() => fn((this as any).value) as any);
665722

666-
return result;
723+
return result as any;
667724
}
668725

669726
rollback(): RollbackFn extends RollbackFunction

src/test/index.test.ts

Lines changed: 56 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -65,25 +65,57 @@ describe("Result", () => {
6565
expect((result as any).value).toBe(123);
6666
});
6767

68-
it("returns an Result.Error when the callbacks throws", () => {
69-
const result = Result.safe(ERROR, () => {
70-
throw new Error("This should fail");
71-
// @ts-ignore
68+
it("allows you to pass in a Error constructor", () => {
69+
class CustomError extends Error {
70+
isCustom = true;
71+
}
72+
const result = Result.safe(CustomError, () => {
73+
throw new Error("random error");
7274
return 1;
7375
});
7476

75-
expect(result.isFailure()).toBe(true);
76-
expect((result as any).error).toBe(ERROR);
77+
expect((result as any).error.isCustom).toBe(true);
7778
});
7879

79-
it("returns an Result.Error when the callbacks throws", async () => {
80-
const result = await Result.safe(ERROR, async () => {
81-
throw new Error("This should fail");
82-
// @ts-ignore
80+
it("allows you to pass in a Error constructor ASYNC", async () => {
81+
class CustomError extends Error {
82+
isCustom = true;
83+
}
84+
const result = await Result.safe(CustomError, async () => {
85+
throw new Error("random error");
8386
return 1;
8487
});
8588

86-
expect(result.isFailure()).toBe(true);
89+
expect((result as any).error.isCustom).toBe(true);
90+
});
91+
92+
it("merges a returned Result when callback is successful", () => {
93+
class CustomError {}
94+
const result = Result.safe(
95+
(): Result<CustomError, number> => {
96+
return Result.ok(1);
97+
}
98+
);
99+
100+
expect((result as any).value).toBe(1);
101+
});
102+
103+
it("merges a returned Result when callback is successful ASYNC", async () => {
104+
const result = await Result.safe(async () => {
105+
return Result.ok(1);
106+
});
107+
108+
expect((result as any).value).toBe(1);
109+
});
110+
111+
it("merges a returned Result when callback is failure", () => {
112+
class CustomError {}
113+
const result = Result.safe(
114+
(): Result<CustomError, number> => {
115+
return Result.error(ERROR);
116+
}
117+
);
118+
87119
expect((result as any).error).toBe(ERROR);
88120
});
89121
});
@@ -429,26 +461,34 @@ describe("Result", () => {
429461

430462
describe("Result#map()", () => {
431463
it("maps the value to another Result on success, returns Result.Error on failure", () => {
432-
const resultOk = Result.ok<Error, number>(1).map(val =>
464+
const resultOk = Result.ok<Error, number>(1).map(val => val * 2);
465+
expect((resultOk as any).value).toBe(2);
466+
467+
const resultNestedOk = Result.ok<Error, number>(1).map(val =>
433468
Result.ok(val * 2)
434469
);
435-
expect((resultOk as any).value).toBe(2);
470+
expect((resultNestedOk as any).value).toBe(2);
436471

437472
const resultError = Result.error<Error, number>(ERROR).map(val =>
438-
Result.ok(val * 2)
473+
Result.ok<Error, number>(val * 2)
439474
);
440475
expect((resultError as any).error).toBe(ERROR);
441476
});
442477

443478
it("maps the value to another Result on success, returns Result.Error on failure ASYNC", async () => {
479+
const resultNestedOk = await Result.ok<Error, number>(1).map(
480+
async val => val * 2
481+
);
482+
expect((resultNestedOk as any).value).toBe(2);
483+
444484
const resultOk = await Result.ok<Error, number>(1).map(async val =>
445-
Result.ok(val * 2)
485+
Result.ok<Error, number>(val * 2)
446486
);
447487
expect((resultOk as any).value).toBe(2);
448488

449489
const resultError = await Result.error<Error, number>(
450490
ERROR
451-
).map(async val => Result.ok(val * 2));
491+
).map(async val => Result.ok<Error, number>(val * 2));
452492
expect((resultError as any).error).toBe(ERROR);
453493
});
454494
});

0 commit comments

Comments
 (0)