Skip to content

Instantly share code, notes, and snippets.

@jhbabon
Created February 11, 2022 12:58
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jhbabon/2e9089b9927c5225628b7bac6ed48204 to your computer and use it in GitHub Desktop.
Save jhbabon/2e9089b9927c5225628b7bac6ed48204 to your computer and use it in GitHub Desktop.
Result and Option types in TypeScript
/**
* 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)
}
/**
* 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