/** * The sad, lonely enum that should be more tightly coupled * to the Option type...but this isn't Rust */ enum OptionType { Some = 'Some', None = 'None', } // ---------------------------------------------------------------------------- // Typeguards to handle Some/None difference // ---------------------------------------------------------------------------- const isOption = <T>(v: any): v is Option<T> => v instanceof Option; class OptionInnerNone { public type: OptionType = OptionType.None; } class OptionInnerSome<T> { public type: OptionType = OptionType.Some; constructor(public value: T) {} } type OptionInnerType<T> = OptionInnerNone | OptionInnerSome<T>; const isSome = <T>(v: OptionInnerType<T>): v is OptionInnerSome<T> => 'value' in v && v.type === OptionType.Some; /** * Rust-style optional type * * Based on https://gist.github.com/s-panferov/575da5a7131c285c0539 */ export class Option<T> { /** * The placeholder for the 'None' value type */ private static _None: Option<any> = new Option(null); /** * Is this a 'Some' or a 'None'? */ private readonly inner: OptionInnerType<T>; private constructor(v?: T) { this.inner = (v !== undefined && v !== null) ? new OptionInnerSome(v) : new OptionInnerNone(); } /** * The equivalent of the Rust `Option`.`None` type */ public static get None(): Option<any> { return Option._None; } public static Some<X>(v: any): Option<X> { return Option.from(v); } public static from<X>(v?: any): Option<X> { return (isOption(v)) ? Option.from(v.unwrap()) : new Option(v); } isSome(): boolean { return isSome(this.inner); } isNone(): boolean { return !this.isSome(); } isSomeAnd(fn: (a: T) => boolean): boolean { return isSome(this.inner) ? fn(this.inner.value) : false; } isNoneAnd(fn: () => boolean): boolean { return this.isNone() ? fn() : false; } map<U>(fn: (a: T) => U): Option<U> { return isSome(this.inner) ? Option.from(fn(this.inner.value)) : Option.None; } mapOr<U>(def: U, f: (a: T) => U): U { return isSome(this.inner) ? f(this.inner.value) : def; } mapOrElse<U>(def: () => U, f: (a: T) => U): U { return isSome(this.inner) ? f(this.inner.value) : def(); } unwrap(): T | never { if (isSome(this.inner)) { return this.inner.value; } console.error('None.unwrap()'); throw 'None.get'; } unwrapOr(def: T): T { return isSome(this.inner) ? this.inner.value : def; } unwrapOrElse(f: () => T): T { return isSome(this.inner) ? this.inner.value : f(); } and<U>(optb: Option<U>): Option<U> { return isSome(this.inner) ? optb : Option.None; } andThen<U>(f: (a: T) => Option<U>): Option<U> { return isSome(this.inner) ? f(this.inner.value) : Option.None; } or(optb: Option<T>): Option<T> { return this.isNone() ? optb : this; } orElse(f: () => Option<T>): Option<T> { return this.isNone() ? f() : this; } toString(): string { const innerValue = (isSome(this.inner)) ? JSON.stringify(this.inner.value) : ''; const prefix = this.inner.type.valueOf(); return (innerValue.length > 0) ? `${prefix} (${innerValue})` : prefix; } } export const { Some, None } = Option; export default Option;