Skip to content

Commit

Permalink
feat(caseError): convert caseError to use a predicate function (#6)
Browse files Browse the repository at this point in the history
* fix(caseError): fixed caseError typings (now works with non-compatible errors and with Tasks errores with only one type)

* feat(caseError): caseError now works with predicates instead of classes

* fix(typings): fixed typings on 'is Something' functions (PR review)

* chore(caseError): removed extra line
  • Loading branch information
dggluz authored and hrajchert committed Aug 27, 2018
1 parent a5e47d0 commit d15a1f2
Show file tree
Hide file tree
Showing 3 changed files with 58 additions and 22 deletions.
57 changes: 42 additions & 15 deletions src/case-error.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { Task, UncaughtError } from '@ts-task/task';
import { assertFork, jestAssertNever, jestAssertUntypedNeverCalled } from './testing-utils';
import { caseError } from './case-error';

class DivisionByZeroError extends Error {
private type = 'DivisionByZeroError';
interface DivisionByZeroError {
errorType: 'DivisionByZeroError';
}

class DontLikeEvenNumbersError extends Error {
Expand All @@ -14,14 +14,30 @@ class DontLikeEvenNumbersError extends Error {
}
}

class NewTypeOfError extends Error {
type = 'NewTypeOfError';
interface NewTypeOfError {
type: 'NewTypeOfError';
}

const isDivisionByZeroError = (err: any): err is DivisionByZeroError =>
err && (err as any).errorType === 'DivisionByZeroError'
;

const isDontLikeEvenNumbersError = (err: any): err is DontLikeEvenNumbersError =>
err instanceof DontLikeEvenNumbersError;
;

const isNewTypeOfError = (err: any): err is NewTypeOfError =>
err && (err as any).type === 'NewTypeOfError';
;

const divisionByZeroError: DivisionByZeroError = {
errorType: 'DivisionByZeroError'
};

// function divideTask (numerator: number, denominator: number) {
function divideTask(numerator: number, denominator: number): Task<number, DivisionByZeroError> {
if (denominator === 0) {
return Task.reject(new DivisionByZeroError())
return Task.reject(divisionByZeroError)
} else {
return Task.resolve(numerator / denominator)
}
Expand All @@ -44,7 +60,9 @@ describe('caseError:', () => {
const task = divideTask(2, 0)

// WHEN: We catch and solve the error
const result = task.catch(caseError(DivisionByZeroError, _ => Task.resolve(-1000)))
const result = task
.catch(caseError(isDivisionByZeroError, _ => Task.resolve(-1000)))
;
// THEN: The resulting type doesn't have the catched error as a posibility
// and the task is resolved with the catched response
result.fork(jestAssertUntypedNeverCalled(cb), assertFork(cb, n => expect(n).toBe(-1000)))
Expand All @@ -56,13 +74,15 @@ describe('caseError:', () => {

// WHEN: We catch and reject with a new error
const result = task.catch(
caseError(DivisionByZeroError, _ => Task.reject(new NewTypeOfError()))
caseError(isDivisionByZeroError, _ => Task.reject({
type: 'NewTypeOfError'
} as NewTypeOfError))
)
// THEN: The resulting type doesn't have the catched error as a posibility
// the resulting type has the new rejected type as a posibility
// and the task is rejected with the new error
result.fork(
assertFork(cb, err => expect(err).toBeInstanceOf(NewTypeOfError)),
assertFork(cb, err => expect(isNewTypeOfError(err)).toBe(true)),
jestAssertUntypedNeverCalled(cb)
)
})
Expand All @@ -73,7 +93,9 @@ describe('caseError:', () => {

// WHEN: We catch the wrong exception trying to reject with a new error
const result = task.catch(
caseError(DivisionByZeroError, _ => Task.reject(new NewTypeOfError()))
caseError(isDivisionByZeroError, _ => Task.reject({
type: 'NewTypeOfError'
} as NewTypeOfError))
)
// THEN: The resulting type doesn't have the unmatched error as a posibility
// the resulting type has the new rejected type as a posibility
Expand All @@ -92,19 +114,24 @@ describe('caseError:', () => {
const result = task
.map(n => `The result is ${n}`)
.catch(
caseError(DivisionByZeroError, _ =>
caseError(isDivisionByZeroError, _ =>
Task.resolve('Could not compute: DivisionByZeroError ocurred')
)
)
.catch(
caseError(DontLikeEvenNumbersError, _ =>
caseError(isDontLikeEvenNumbersError, _ =>
Task.resolve('Could not compute: DontLikeEvenNumbersError ocurred')
)
)
.catch(
caseError(UncaughtError, err => Task.resolve(`Could not compute: UncaughtError ${err}`))
caseError(
(err: any): err is UncaughtError =>
err instanceof UncaughtError,
err =>
Task.resolve(`Could not compute: UncaughtError ${err}`)
)
)
;
;
// THEN: The resulting type doesn't have the catched errors
// and the task is resolved with the mapped answer
result.fork(jestAssertNever(cb), assertFork(cb, s => expect(s).toBe('The result is 5')));
Expand All @@ -117,14 +144,14 @@ describe('caseError:', () => {
// WHEN: We catch an imposible exception
const result = task.catch(
caseError(
DontLikeEvenNumbersError, // TODO: It would be nice to see this fail compilation as it is not possible that
isDontLikeEvenNumbersError, // TODO: It would be nice to see this fail compilation as it is not possible that
// task fails with DontLikeEvenNumbersError
_ => Task.resolve(0)
)
)
// THEN: The task is rejected with the original error
result.fork(
assertFork(cb, err => expect(err).toBeInstanceOf(DivisionByZeroError)),
assertFork(cb, err => expect(isDivisionByZeroError(err)).toBe(true)),
jestAssertUntypedNeverCalled(cb)
)
})
Expand Down
10 changes: 5 additions & 5 deletions src/case-error.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import { Task, UncaughtError } from '@ts-task/task'

export type Constructor<T> = { new (...args: any[]): T }
import { Task } from '@ts-task/task'

export type ErrorHandler<ErrorToHandle, TResult, EResult> = (err: ErrorToHandle) => Task<TResult, EResult>

export type ErrorPredicate <ErrorToHandle> = <T> (err: T | ErrorToHandle) => err is ErrorToHandle;

export function caseError<ErrorToHandle, TResult, EResult>(
ErrorType: Constructor<ErrorToHandle>,
errorPredicate: ErrorPredicate<ErrorToHandle>,
errorHandler: ErrorHandler<ErrorToHandle, TResult, EResult>
) {
return function <InputError> (
err: InputError | ErrorToHandle
): Task<TResult, EResult | Exclude<InputError, ErrorToHandle>> {
// If the error is of the type we are looking for (E)
if (err instanceof ErrorType) {
if (errorPredicate(err)) {
// Transform the error
return errorHandler(err);
} else {
Expand Down
13 changes: 11 additions & 2 deletions test/types/case-error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,20 @@ import { Task } from '@ts-task/task';
// It has a `NoNegativesError` public property because TypeScript inference is structural
class NoNegativesError extends Error {
NoNegativesError = 'NoNegativesError';

constructor(public negativeNumber: number) {
super(`Ugh! ${negativeNumber} is soooo negative! >:(`);
}
}

// rejectNegative is a function that will return a Task possible rejected with a NoNegativesError
const rejectNegative = (x: number): Task<number, NoNegativesError> =>
x >= 0 ? Task.resolve(x) : Task.reject(new NoNegativesError())
x >= 0 ? Task.resolve(x) : Task.reject(new NoNegativesError(x))
;

// isNoNegativesError is a function that tells us if an error is a NoNegativesError
const isNoNegativesError = (err: any): err is NoNegativesError =>
err instanceof NoNegativesError
;

// We will tests our `caseError` function with two variables
Expand All @@ -30,7 +39,7 @@ const anotherNumber = aNumber // $ExpectType Task<number, NoNegativesError | Unc

// We will also need a function `fixNoNegativesError` that should handle (and resolve)
// the `NegativesError` case, delegating in `caseError`.
const fixNoNegativesError = caseError(NoNegativesError, _ => Task.resolve(0));
const fixNoNegativesError = caseError(isNoNegativesError, _ => Task.resolve(0));

// We try our `fixNoNegativesError` on both Tasks
const result = aNumber.catch(fixNoNegativesError);
Expand Down

0 comments on commit d15a1f2

Please sign in to comment.