Skip to content

Instantly share code, notes, and snippets.

@waspeer
Last active December 14, 2020 18:00
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save waspeer/049d7be66f96609aa863b074af846ba9 to your computer and use it in GitHub Desktop.
Save waspeer/049d7be66f96609aa863b074af846ba9 to your computer and use it in GitHub Desktop.
/**
/* 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
}
});
@florianbepunkt
Copy link

Could you also reference the Either monad? Using the version from the current blog post makes the TS compiler fail.

@waspeer
Copy link
Author

waspeer commented Dec 14, 2020

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