monads-co
Version:
An implementation of Haskell's type classes in TS
177 lines (153 loc) • 4.32 kB
text/typescript
import { Monad, MonadHKT } from './Monad'
export interface OptionalHKT extends MonadHKT {
output: Optional<this["input"]>
}
export class Optional<ValueT> extends Monad<ValueT> {
constructor(public _success: boolean, public _value: ValueT | undefined | null) {
super()
}
/**
* Used to wrap a function that can potentially return null or undefined in a safe wrapper
* ```ts
* const safeQuerySelector = Optional.wrap(document.querySelector)
* ```
* @param fn
*/
static wrap<FnT extends (...args: any[]) => any>(fn: FnT) {
return (...args: Parameters<FnT>) => {
const out = fn(...args)
if (out === undefined || out === null) {
return None<NonNullable<ReturnType<FnT>>>()
} else {
return Some<NonNullable<ReturnType<FnT>>>(out)
}
}
}
/**
* If a value is held, this uses the mapping function to transform it, and returns Optional<New Value>
* otherwise, it returns None()
* @param mapping
*/
map<B>(mapping: (value: ValueT) => B): Optional<B> {
if (this._success) {
return Some(mapping(this._value as ValueT))
} else {
return None()
}
}
amap<B>(mapping: Optional<(value: ValueT) => B>): Optional<B> {
if (this._success) {
if (mapping._success) {
return Some(
(mapping._value as (value: ValueT) => B)(this._value as ValueT)
)
}
return None()
}
return None()
}
pure<B>(value: B): Optional<B> {
return Some(value)
}
/**
* If a value is held the mapping is preformed on it, otherwise None() is returned. If you want to just transform
* ValueT to a new Value, use `.map`
* @param mapping
*/
then<B>(mapping: (value: ValueT) => Optional<B>): Optional<B> {
if (this._success) {
return mapping(this._value as ValueT)
}
return None()
}
// Result specific methods
/**
* Returns the value is defined, otherwise it returns `defaultValue`
* @param defaultValue
*/
or<B>(defaultValue: B): ValueT | B {
if (this._success) {
return this._value as ValueT
}
return defaultValue
}
/**
* If None() replace it with a value
* @param mapping
*/
catch<NewErrorT>(mapping: () => ValueT): Optional<ValueT> {
if (this._success) {
return Some(this._value as ValueT)
}
return Some(mapping())
}
/**
* If None() replaces it with the result of the mapping
* @param mapping
*/
catchThen<NewErrorT>(mapping: () => Optional<ValueT>): Optional<ValueT> {
if (this._success) {
return Some(this._value as ValueT)
}
return mapping()
}
/**
* Returns the value if Some(), otherwise it throws a reference error.
* This is the nuclear option, `.or(default)` should be preferred.
*/
unwrap(): ValueT {
if (this._success) {
return this._value as ValueT
}
// Kaboom, functional land just died :(
throw new ReferenceError("When Optional<T> was unwrapped, no value was found")
}
}
/**
* Used to construct Some value, for example:
*
* @example
* ```ts
* // Gets all but the start of the string; IE "string" -> "tring"
* function getStringTail(input: string): Optional<string> {
* if (input.length <= 1) {
* return None()
* } else {
* return Some(input.slice(1))
* }
* }
* ```
* @param value
*/
export function Some<ValueT>(value: ValueT) {
return new Optional<ValueT>(true, value)
}
/**
* Used to construct Some value, for example:
*
* @example
* ```ts
* // Gets all but the start of the string; IE "string" -> "tring"
* function getStringTail(input: string): Optional<string> {
* if (input.length <= 1) {
* return None()
* } else {
* return Some(input.slice(1))
* }
* }
* ```
*/
export function None<ValueT>() {
return new Optional<ValueT>(false, null)
}
/**
* Like Some() or None() but it doesn't mind if you give it a value or not.
* @param value
*/
export function Maybe<ValueT>(value?: ValueT | undefined | null): Optional<NonNullable<ValueT>> {
if (value) {
return Some(value as NonNullable<ValueT>)
} else {
return None()
}
}