Created
February 11, 2022 12:58
-
-
Save jhbabon/2e9089b9927c5225628b7bac6ed48204 to your computer and use it in GitHub Desktop.
Result and Option types in TypeScript
This file contains 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
/** | |
* Type Option represents an optional value: every option is either Some and contains a value, | |
* or None, and does not. | |
* | |
* The main idea behind Option is to prevent the abuse of null|undefined through the code | |
* and to enforce safe value checking through the type system. The absence of something is | |
* explicit thanks to None. | |
* | |
* @example Using basic is* methods | |
* | |
* const message: Option<string> = messages.find('myid') | |
* if (message.isSome()) { | |
* // the message was found | |
* console.log(message.unwrap()) | |
* } | |
* | |
* @example Using basic pattern matching | |
* | |
* const message: Option<string> = messages.find('myid') | |
* message.match({ | |
* some: (m: string): void => console.log(m), | |
* none: (): void => console.log('Not Found'), | |
* }) | |
*/ | |
type Mapper<T, A> = (value: T) => A | |
type AsyncMapper<T, A> = (value: T) => A | Promise<A> | |
type DefaultMapper<A> = () => A | |
type AsyncDefaultMapper<A> = () => A | Promise<A> | |
type Match<T, A> = { some: Mapper<T, A>; none: DefaultMapper<A> } | |
interface IOption<T> { | |
/** | |
* Type Guard function that checks if an instance is of type Some<T> | |
*/ | |
isSome(): this is Some<T> | |
/** | |
* Type Guard function that checks if an instance is of type None | |
*/ | |
isNone(): this is None | |
/** | |
* Some<T>: Return the inner T value | |
* None: throw an error. This makes it an usafe operation on None type | |
*/ | |
unwrap(): T | |
/** | |
* Some<T>: Return the inner T value | |
* None: return the given argument (other) as default value | |
*/ | |
unwrapOr(other: T): T | |
/** | |
* Some<T>: Return the inner T value | |
* None: return the return value of the given default mapper function (other). | |
* This is done for lazy evaluation. | |
*/ | |
unwrapOrElse(other: DefaultMapper<T>): T | |
/** | |
* Some<T>: Transform the inner value T with the map function and return a new Some<A> instance | |
* None: Return None without calling the mapper function | |
*/ | |
map<A>(fn: (value: T) => A): Option<A> | |
/** | |
* Some<T>: Return Some<T> without calling the mapper function | |
* None: return the given argument (other) as default value | |
*/ | |
mapOr<A>(other: A, fn: Mapper<T, A>): Option<A> | |
/** | |
* Some<T>: Return Some<T> without calling the mapper function | |
* None: return the return value of the given default mapper function (other). | |
* This is done for lazy evaluation. | |
*/ | |
mapOrElse<A>(other: DefaultMapper<A>, fn: Mapper<T, A>): Option<A> | |
/** | |
* Async version of `map()`. Once the promise is resolved, it returns a new Option | |
*/ | |
asyncMap<A>(fn: AsyncMapper<T, A>): AsyncOption<A> | |
/** | |
* Async version of `mapOr()`. Once the promise is resolved, it returns a new Option | |
*/ | |
asyncMapOr<A>(other: A, fn: AsyncMapper<T, A>): AsyncOption<A> | |
/** | |
* Async version of `mapOrElse()`. Once the promise is resolved, it returns a new Option | |
*/ | |
asyncMapOrElse<A>(other: AsyncDefaultMapper<A>, fn: AsyncMapper<T, A>): AsyncOption<A> | |
/** | |
* Simple pattern matching on Some<T> and None values. | |
* | |
* This is helpful to avoid big if...else branches and favors composition | |
* | |
* @example | |
* | |
* const response: HttpResponse = option.match({ | |
* some: (data) => new HttpResponse({ status: 200, body: toJSON(data) }), | |
* none: (e) => new HttpResponse({ status: 404, body: 'Not Found' }) | |
* }) | |
*/ | |
match<A>(branches: Match<T, A>): A | |
} | |
/** | |
* Represents the existence of a value | |
*/ | |
export class Some<T> implements IOption<T> { | |
constructor(readonly value: T) { | |
this.value = value | |
} | |
isSome(): this is Some<T> { | |
return true | |
} | |
isNone(): this is None { | |
return false | |
} | |
unwrap(): T { | |
return this.value | |
} | |
unwrapOr(_other: T): T { | |
return this.value | |
} | |
unwrapOrElse(_other: DefaultMapper<T>): T { | |
return this.value | |
} | |
map<A>(fn: Mapper<T, A>): Option<A> { | |
return new Some(fn(this.value)) | |
} | |
mapOr<A>(_other: A, fn: Mapper<T, A>): Option<A> { | |
return new Some(fn(this.value)) | |
} | |
mapOrElse<A>(_other: DefaultMapper<A>, fn: Mapper<T, A>): Option<A> { | |
return new Some(fn(this.value)) | |
} | |
async asyncMap<A>(fn: AsyncMapper<T, A>): AsyncOption<A> { | |
return new Some(await fn(this.value)) | |
} | |
async asyncMapOr<A>(_other: A, fn: AsyncMapper<T, A>): AsyncOption<A> { | |
return new Some(await fn(this.value)) | |
} | |
async asyncMapOrElse<A>(_other: AsyncDefaultMapper<A>, fn: AsyncMapper<T, A>): AsyncOption<A> { | |
return new Some(await fn(this.value)) | |
} | |
match<A>(branches: Match<T, A>): A { | |
return branches.some(this.value) | |
} | |
} | |
/** | |
* Represents the absence of a value | |
*/ | |
export class None implements IOption<never> { | |
isSome(): this is Some<never> { | |
return false | |
} | |
isNone(): this is None { | |
return true | |
} | |
unwrap(): never { | |
throw new Error('Called `Option#unwrap()` on a `None` value') | |
} | |
unwrapOr<T>(other: T): T { | |
return other | |
} | |
unwrapOrElse<T>(other: DefaultMapper<T>): T { | |
return other() | |
} | |
map<A>(_fn: Mapper<never, A>): Option<A> { | |
return new None() | |
} | |
mapOr<A>(other: A, _fn: Mapper<never, A>): Option<A> { | |
return new Some(other) | |
} | |
mapOrElse<A>(other: DefaultMapper<A>, _fn: Mapper<never, A>): Option<A> { | |
return new Some(other()) | |
} | |
async asyncMap<A>(_fn: AsyncMapper<never, A>): AsyncOption<A> { | |
return new None() | |
} | |
async asyncMapOr<A>(other: A, _fn: AsyncMapper<never, A>): AsyncOption<A> { | |
return new Some(other) | |
} | |
async asyncMapOrElse<A>(other: AsyncDefaultMapper<A>, _fn: AsyncMapper<never, A>): AsyncOption<A> { | |
return new Some(await other()) | |
} | |
match<A>(branches: Match<never, A>): A { | |
return branches.none() | |
} | |
} | |
export type Option<T> = Some<T> | None | |
export type AsyncOption<T> = Promise<Option<T>> | |
// Use Empty to create Option variables that don't hold any value | |
export const EMPTY: unique symbol = Symbol('option/some/empty') | |
export type Empty = typeof EMPTY | |
/** | |
* Return a new Some<T> instance | |
*/ | |
export function some<T>(value: T): Some<T> { | |
return new Some(value) | |
} | |
/** | |
* Return a new None instance | |
*/ | |
export function none(): None { | |
return new None() | |
} | |
export function isSome<T>(o: unknown): o is Some<T> { | |
return o instanceof Some | |
} | |
export function isNone(o: unknown): o is None { | |
return o instanceof None | |
} | |
export function isOption<T>(o: unknown): o is Option<T> { | |
return isSome<T>(o) || isNone(o) | |
} |
This file contains 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
/** | |
* Result<T, E> is the type used for returning and propagating errors. | |
* It is an union with the variants, Ok<T>, representing success and containing a value, | |
* and Err<E>, representing error and containing an error value. | |
* | |
* Functions return Result whenever errors are expected and recoverable. | |
* | |
* @example Using basic is* methods | |
* | |
* const values: Result<Record<string, string>[], DbError> = db.query(`SELECT * FROM my_table`) | |
* | |
* if (values.isErr()) { | |
* await db.reconnect() | |
* } else { | |
* values.unwrap().map(console.log) | |
* } | |
* | |
* @example Using basic pattern matching | |
* | |
* const values: Result<Record<string, string>[], DbError> = db.query(`SELECT * FROM my_table`) | |
* | |
* await values.match({ | |
* ok: (values: Record<string, string>[]): void => values.map(console.log), | |
* err: (_e: DbError): void => db.reconnect(), | |
* }) | |
*/ | |
type Mapper<T, A> = (value: T) => A | |
type AsyncMapper<T, A> = (value: T) => A | Promise<A> | |
type ErrorMapper<E, X> = (error: E) => X | |
type AsyncErrorMapper<E, X> = (error: E) => X | Promise<X> | |
type Match<T, A, E> = { ok: Mapper<T, A>; err: ErrorMapper<E, A> } | |
interface IResult<T, E> { | |
/** | |
* Type Guard function that checks if an instance is of type Ok<T> | |
*/ | |
isOk(): this is Ok<T> | |
/** | |
* Type Guard function that checks if an instance is of type Err<E> | |
*/ | |
isErr(): this is Err<E> | |
/** | |
* Ok<T>: return the inner T value | |
* Err<E>: throw the error E. This makes it an usafe operation on Err<E> type | |
*/ | |
unwrap(): T | |
/** | |
* Ok<T>: return the inner T value | |
* Err<E>: return the given argument (other) as default value | |
*/ | |
unwrapOr(other: T): T | |
/** | |
* Ok<T>: Transform the inner value T with the map function and return a new Ok<A> instance | |
* Err<E>: Return Err<E> without calling the mapper function | |
*/ | |
map<A>(fn: Mapper<T, A>): Result<A, E> | |
/** | |
* Ok<T>: Return Ok<T> without calling the mapper function | |
* Err<E>: Transform the inner error E with the map error function and return a new Err<X> instance | |
*/ | |
mapErr<X>(fn: ErrorMapper<E, X>): Result<T, X> | |
/** | |
* Async version of `map()`. Once the promise is resolved, it returns a new Result | |
*/ | |
asyncMap<A>(fn: AsyncMapper<T, A>): AsyncResult<A, E> | |
/** | |
* Async version of `mapErr()`. Once the promise is resolved, it returns a new Result | |
*/ | |
asyncMapErr<X>(fn: AsyncErrorMapper<E, X>): AsyncResult<T, X> | |
/** | |
* Simple pattern matching on Ok<T> and Err<E> values. | |
* | |
* This is helpful to avoid big if...else branches and favors composition | |
* | |
* @example | |
* | |
* const response: HttpResponse = result.match({ | |
* ok: (data) => new HttpResponse({ status: 200, body: toJSON(data) }), | |
* err: (e) => new HttpResponse({ status: 500, body: e.message }) | |
* }) | |
*/ | |
match<A>(branches: Match<T, A, E>): A | |
} | |
/** | |
* Represents success | |
*/ | |
export class Ok<T> implements IResult<T, never> { | |
constructor(readonly value: T) { | |
this.value = value | |
} | |
isOk(): this is Ok<T> { | |
return true | |
} | |
isErr(): this is Err<never> { | |
return false | |
} | |
unwrap(): T { | |
return this.value | |
} | |
unwrapOr(_other: T): T { | |
return this.value | |
} | |
map<A>(fn: Mapper<T, A>): Result<A, never> { | |
return new Ok(fn(this.value)) | |
} | |
mapErr<X>(_fn: ErrorMapper<never, X>): Result<T, X> { | |
return new Ok(this.value) | |
} | |
async asyncMap<A>(fn: AsyncMapper<T, A>): AsyncResult<A, never> { | |
return new Ok(await fn(this.value)) | |
} | |
async asyncMapErr<X>(_fn: AsyncErrorMapper<never, X>): AsyncResult<T, X> { | |
return new Ok(this.value) | |
} | |
match<A>(branches: Match<T, A, never>): A { | |
return branches.ok(this.value) | |
} | |
} | |
/** | |
* Represents failure | |
*/ | |
export class Err<E> implements IResult<never, E> { | |
constructor(readonly error: E) { | |
this.error = error | |
} | |
isOk(): this is Ok<never> { | |
return false | |
} | |
isErr(): this is Err<E> { | |
return true | |
} | |
unwrap(): never { | |
throw this.error | |
} | |
unwrapOr<T>(other: T): T { | |
return other | |
} | |
map<A>(_fn: Mapper<never, A>): Result<A, E> { | |
return new Err(this.error) | |
} | |
mapErr<X>(fn: ErrorMapper<E, X>): Result<never, X> { | |
return new Err(fn(this.error)) | |
} | |
async asyncMap<A>(_fn: AsyncMapper<never, A>): AsyncResult<A, E> { | |
return new Err(this.error) | |
} | |
async asyncMapErr<X>(fn: AsyncErrorMapper<E, X>): AsyncResult<never, X> { | |
return new Err(await fn(this.error)) | |
} | |
match<A>(branches: Match<never, A, E>): A { | |
return branches.err(this.error) | |
} | |
} | |
export type Result<T, E = Error> = Ok<T> | Err<E> | |
export type AsyncResult<T, E = Error> = Promise<Result<T, E>> | |
/** | |
* Build a new Ok<T> instance | |
*/ | |
export function ok<T>(value: T): Ok<T> { | |
return new Ok(value) | |
} | |
/** | |
* Build a new Err<E> instance | |
*/ | |
export function err<E = Error>(error: E): Err<E> { | |
return new Err(error) | |
} | |
export function isErr<E>(r: unknown): r is Err<E> { | |
return r instanceof Err | |
} | |
export function isOk<T>(r: unknown): r is Ok<T> { | |
return r instanceof Ok | |
} | |
/** | |
* Type guard around Result values | |
*/ | |
export function isResult<T, E>(r: unknown): r is Result<T, E> { | |
return isOk<T>(r) || isErr<E>(r) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment