diff --git a/src/common/option.ts b/src/common/option.ts index 43a8603..1a904e0 100644 --- a/src/common/option.ts +++ b/src/common/option.ts @@ -13,19 +13,19 @@ enum OptionType { const isOption = (v: any): v is Option => v instanceof Option; -class OptionInnerNone { +class OptNone { public type: OptionType = OptionType.None; } -class OptionInnerSome { +class OptSome { public type: OptionType = OptionType.Some; constructor(public value: T) {} } -type OptionInnerType = OptionInnerNone | OptionInnerSome; +type OptType = OptNone | OptSome; -const isSome = (v: OptionInnerType): v is OptionInnerSome => +const isSome = (v: OptType): v is OptSome => 'value' in v && v.type === OptionType.Some; /** @@ -42,90 +42,211 @@ export class Option { /** * Is this a 'Some' or a 'None'? */ - private readonly inner: OptionInnerType; + private readonly inner: OptType; private constructor(v?: T) { this.inner = (v !== undefined && v !== null) - ? new OptionInnerSome(v) - : new OptionInnerNone(); + ? new OptSome(v) + : new OptNone(); } /** - * The equivalent of the Rust `Option`.`None` type + * The equivalent of the Rust `Option`.`None` enum value */ public static get None(): Option { return Option._None; } - public static Some(v: any): Option { - return Option.from(v); + /** + * The equivalent of the Rust `Option`.`Some` enum value + * + * If the value passed is null or undefined, this will throw an Error + * + * @param v The value to wrap + */ + public static Some(v: any): Option | never { + const maybeSome: Option = Option.from(v); + + if (maybeSome.isNone()) { + throw new Error('Cannot create Some with an empty value'); + } else { + return maybeSome; + } } + /** + * Create a new `Option` + * + * If the value is null or undefined, the `Option` will have a `None` type + * + * @param v The value to wrap + */ public static from(v?: any): Option { return (isOption(v)) ? Option.from(v.unwrap()) : new Option(v); } + /** + * The wrapped value is not null or undefined + */ isSome(): boolean { return isSome(this.inner); } + /** + * The wrapped value is null or undefined + */ isNone(): boolean { return !this.isSome(); } + /** + * Check if the current `Option` is `Some`. If it is, + * return the value of the passed function. + * + * Otherwise, returns false + * + * @param fn A boolean check to run on the wrapped value + */ isSomeAnd(fn: (a: T) => boolean): boolean { return isSome(this.inner) ? fn(this.inner.value) : false; } + /** + * Check if the current `Option` is `None`. If it is, + * return the value of the passed function. + * + * Otherwise, return false + * + * @param fn A function returning a boolean value + */ isNoneAnd(fn: () => boolean): boolean { return this.isNone() ? fn() : false; } + /** + * Transform the inner value of the `Option` with the passed function. + * If this `Option` is `Some`, a new `Option` with the transformed value + * is returned. Otherwise `None` is returned + * + * @param fn A function that takes the inner value of the `Option` and returns a new one + */ map(fn: (a: T) => U): Option { return isSome(this.inner) ? Option.from(fn(this.inner.value)) : Option.None; } - mapOr(def: U, f: (a: T) => U): U { - return isSome(this.inner) ? f(this.inner.value) : def; + /** + * If this `Option` is `Some`, return the transformed inner value via the passed function. + * + * Otherwise, return the passed default value + * + * @param def The default value to return if this `Option` is `None` + * @param fn A function that takes the inner value of this `Option` and returns a new value + */ + mapOr(def: U, fn: (a: T) => U): U { + return isSome(this.inner) ? fn(this.inner.value) : def; } - mapOrElse(def: () => U, f: (a: T) => U): U { - return isSome(this.inner) ? f(this.inner.value) : def(); + /** + * If this `Option` is `Some`, return the transformed inner value via the passed function (fn). + * + * Otherwise run the function (def) to return a value + * + * @param def A function to return a value if this `Option` is `None` + * @param fn A function that takes the inner value of this `Option` and returns a new value + */ + mapOrElse(def: () => U, fn: (a: T) => U): U { + return isSome(this.inner) ? fn(this.inner.value) : def(); } - unwrap(): T | never { + /** + * Return the inner value if not `None`. + * Otherwise, throw a new exception with the passed message + * + * @param err + */ + assert(err: string): T | never { if (isSome(this.inner)) { return this.inner.value; } - console.error('None.unwrap()'); - throw 'None.get'; + throw Error(err); } + /** + * Return the inner value if is `Some`. + * + * If `None`, throws an exception. + */ + unwrap(): T | never { + return this.assert("Called unwrap on a 'None'"); + } + + /** + * Return the inner value if is `Some`, + * Otherwise, return the passed default value + * + * @param def Value to return on `None` value + */ unwrapOr(def: T): T { return isSome(this.inner) ? this.inner.value : def; } + /** + * Return the inner value if is `Some`, + * Otherwise, return the value generated by the passed function + * + * @param f Function to run on `None` value + */ unwrapOrElse(f: () => T): T { return isSome(this.inner) ? this.inner.value : f(); } + /** + * Check if this `Option` and the passed option are both `Some`, + * otherwise return `None` + * + * @param optb Another `Option` to check + */ and(optb: Option): Option { return isSome(this.inner) ? optb : Option.None; } + /** + * Check if this `Option` is `Some`. If it is, run the passed + * function with the wrapped value. + * + * Otherwise, return None + * + * @param f function to run on the wrapped value + */ andThen(f: (a: T) => Option): Option { return isSome(this.inner) ? f(this.inner.value) : Option.None; } + /** + * Check if this `Option` is `None`. If it is, return the passed option. + * + * @param optb The `Option` to return if this `Option` is `None` + */ or(optb: Option): Option { return this.isNone() ? optb : this; } + /** + * Check if this `Option` is `None`. If it is, return the passed function. + * + * Otherwise, return this `Option` + * + * @param f A function to return a different `Option` + */ orElse(f: () => Option): Option { return this.isNone() ? f() : this; } + /** + * Create a string representation of the `Option`, + * mostly for debugging + */ toString(): string { const innerValue = (isSome(this.inner)) ? JSON.stringify(this.inner.value)