Last active
December 14, 2020 18:00
-
-
Save waspeer/049d7be66f96609aa863b074af846ba9 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
/* An attempt to make the error handling process described on Khalil Stemmler's excellent | |
/* blog a little somewhat simpler. | |
/* https://khalilstemmler.com/articles/enterprise-typescript-nodejs/functional-error-handling/ | |
/** | |
// Left and Right classes, but more specific to error handling. | |
class Failure<L, A = any> { | |
readonly error: L; | |
constructor(error: L) { | |
this.error = error; | |
} | |
get value(): A { | |
throw new Error('unable to retrieve value from failed result'); | |
} | |
isFailure(): this is Failure<L, A> { | |
return true; | |
} | |
isSuccess(): this is Success<L, A> { | |
return false; | |
} | |
} | |
class Success<L, A> { | |
private readonly _value?: A; | |
constructor(value?: A) { | |
this._value = value; | |
} | |
get value(): A { | |
return this.value as A; | |
} | |
isFailure(): this is Failure<L, A> { | |
return false; | |
} | |
isSuccess(): this is Success<L, A> { | |
return true; | |
} | |
} | |
// Result is now just a namespace for some helper functions | |
export namespace Result { | |
export function fail<L, A>(l: L): Either<L, A> { | |
return new Failure(l); | |
}; | |
export function ok<L, A>(a?: A): Either<L, A> { | |
return new Success<L, A>(a); | |
}; | |
export function combine<L, A>(results: Either<L, A>[]): Either<L, A> { | |
return results.reduce((combinedResult, result) => { | |
if (combinedResult.isFailure()) return combinedResult; | |
return result; | |
}, Result.ok()); | |
} | |
} | |
// Utility class for a generic domain error | |
interface DomainErrorDTO { | |
message: string; | |
error?: any; | |
} | |
export abstract class DomainError extends Failure<DomainErrorDTO> {}; | |
// Convenient type alias for a result that could be an error | |
export type ErrorOr<T> = Either<DomainError, T>; | |
// Example usage: | |
// Error creation | |
class UsernameTakenError extends DomainError { | |
public constructor(username: string) { | |
super({ | |
message: `The username "${username}" has already been taken.` | |
}); | |
} | |
public static create(username: string): UsernameTakenError { | |
return new UsernameTakenError(username); | |
} | |
} | |
// In a use case | |
class CreateUserUseCase { | |
private userRepo: any; | |
constructor(userRepo: any) { | |
this.userRepo = userRepo; | |
} | |
public async execute(request: Request): Promise<ErrorOr<void>> { | |
const { username } = request; | |
const userOrNull = await this.userRepo.findOne({ | |
username | |
}); | |
if (userOrNull !== null) { | |
return new CreateUserError.UsernameTaken(username); | |
} | |
// create user | |
return Result.ok(); | |
} | |
} | |
// Typescript still infers the right type | |
const createUserUseCase = new CreateUserUseCase({}); | |
createUserUseCase.execute({ username: 'asdf' }).then(result => { | |
if (result.isFailure()) { | |
console.error(result.error); // Type: DomainErrorDTO | |
} else { | |
console.log(result.value); // Type: void | |
} | |
}); |
I think it must have been this one:
/**
* Repesents a type that is either a successful result or an error.
*
* @template T - The type of the unsuccesful result.
* @template S - The type of the successful result.
*/
export type Either<T extends DomainErrorObject<any>, S> = Failure<T, S> | Success<T, S>;
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Could you also reference the Either monad? Using the version from the current blog post makes the TS compiler fail.