import { err, ok, Result } from "@shared/utils/result.ts"; type OptionJSON = { tag: "some"; value: T } | { tag: "none" }; interface IOption { /** * Checks if the `Option` is a `Some`. * ```typescript * // Example * const a = some(5) * const b = none * a.isSome() // true b.isSome() // false * ``` * @returns {boolean} `true` if the Option is a Some, otherwise `false`. */ isSome(): this is Some; ifSome(fn: (value: T) => void): Option; /** * Checks if the `Option` is a `None`. * ```typescript * // Example * const a = some(5); * const b = none; * a.isNone(); // false * b.isNone(); // true * ``` * * @returns {boolean} `true` if the Option is a None, otherwise `false`. */ isNone(): this is None; ifNone(fn: () => void): Option; /** * Uses the function `fn` to map a value of type `T`, stored inside of the `Option`, to a value of type `U`. Then wraps a mapped value to the new `Option` and returns it. The difference from a `.flatMap()` is that `.flatMap()` does not wraps a mapped value into the `Option`, but rather requires `fn` to take care of it. * * ```typescript * // Example * const a = some(5); * const b = a.map((value) => {value + 5}); // => Some(10) * * // compare .map() and .flatMap(): * const mapFn = (value: number) => some(value + 5); * const c = a.map(mapFn); // => Some(Some(10)) * const d = a.flatMap(mapFn); // => Some(10) * ``` * @template `U` - The type of the result of the mapping. * @param {Function} `fn` - The function to apply to the value inside the Option. * @returns {Option} a new `Option` wrapping the mapped value. */ map(fn: (value: T) => U): Option; /** * Uses the function `fn` to map a value of type `T`, stored inside of the `Option`, to a value of type `Option` and returns it. The difference from a `.map()` is that `.flatMap()` does not wrap the mapped value `U` into the Option by itself, but rather requires a function `fn` to take care of it. * ```typescript * // Example * const a = some(5); * const b = a.flatMap((value) => some(value + 5)); // Some(10) * * // compare .map() and .flatMap(): * const mapFn = (value: number) => some(value + 5); * const c = a.map(mapFn); // Some(Some(10)) * const d = a.flatMap(mapFn); // Some(10) * ``` * @param {Function} The function `fn` that takes the value inside the `Option` and returns a new `Option`. * @returns {Option} A new `Option` wrapping the result of the flatMap operation. */ flatMap(fn: (value: T) => Option): Option; andThen(fn: (value: T) => Option): Option; /** * **UNSAFE** method for extracting value `T` from an `Option`. * - If the `Option` is `Some` => returns value `T` * - If the `Option` is `None` => throws a Error * * Should not be used in production * ```typescript * // Example * const a = some(5); * const b = none; * const unwrappedA = a.unwrap(); // 5 * const unwrappedB = a.unwrap(); // Throws error * ``` * @returns {T} The value inside the Option. * @throws {Error} If the Option is a None, an error is thrown. */ unwrap(): T; /** * safe method for extracting value `T` from an `Option`. * - If the `Option` is `Some` => returns value `T` * - If the `Option` is `None` => returns `defaultValue` * ```typescript * // Example * const a = some(5); * const b = none; * const unwrappedA = a.getOrElse(10); // 5 * const unwrappedB = a.getOrElse(10); // 10 * ``` * @param {T} defaultValue The value to return if the Option is a None. * @returns {T} The value `T` inside the Option if it's a `Some`, otherwise the `defaultValue`. */ unwrapOr(defaultValue: U): T | U; unwrapOrElse(fn: () => U): T | U; or(optb: Option): Option; orElse(fn: () => Option): Option; /** * Matches on the `Option` and applies the corresponding function for `Some` or `None`. * - If the `Option` is `Some` => applies the first function `some` * - If the Option is None => applies the second function `none` * ```typescript * // Example * const a = some(5); * const b = none; * * const someFn = (value) => value + 3; * const noneFn = () => 10 * * const matchedA = a.match(someFn, noneFn) // 8 * const matchedB = b.match(someFn, noneFn) // 10 * ``` * @param {Function} some The function to apply if the Option is a Some. * @param {Function} none The function to apply if the Option is a None. * @returns {unknown} The result of the matching function applied. */ match(some: (value: T) => A, none: () => B): A | B; toNullable(): T | null; toBoolean(): boolean; okOrElse(errFn: () => E): Result; toJSON(): OptionJSON; } /** * Represents a `Some` value in the Option type, wrapping a value of type `T`. * @template `T` The type of the value inside the `Some`. */ export class Some implements IOption { public readonly tag = "some"; /** * Creates a new `Some` instance. * @param {T} The value `T` to wrap inside the `Some`. */ constructor(public readonly value: T) { Object.defineProperties(this, { tag: { writable: false, enumerable: false, }, }); } isSome(): this is Some { return true; } ifSome(fn: (value: T) => void): this { fn(this.value); return this; } isNone(): this is None { return false; } ifNone(fn: () => void): Option { return this; } map(fn: (value: T) => U): Option { return new Some(fn(this.value)); } flatMap(fn: (value: T) => Option): Option { return fn(this.value); } andThen(fn: (value: T) => Option): Option { return fn(this.value); } unwrap(): T { return this.value; } unwrapOr(defaultValue: U): T | U { return this.value; } unwrapOrElse(fn: () => U): T | U { return this.value; } or(optb: Option): Option { return this; } orElse(fn: () => Option): Option { return this; } match(some: (value: T) => A, none: () => B): A | B { return some(this.value); } toString() { return `Some(${this.value})`; } toJSON(): OptionJSON { return { tag: "some", value: this.value }; } toNullable(): T | null { return this.value; } toBoolean(): boolean { return true; } okOrElse(errFn: () => E): Result { return ok(this.value); } } /** * Represents a `None` value in the Option type, indicating no value. * @template `T` The type that would be inside the Option, but it is not used because this is a None. */ export class None implements IOption { public readonly tag = "none"; /** * Creates a new `None` instance. */ constructor() { Object.defineProperties(this, { tag: { writable: false, enumerable: false, }, }); } isSome(): this is Some { return false; } ifSome(fn: (value: T) => void): Option { return this; } isNone(): this is None { return true; } ifNone(fn: () => void): Option { fn(); return this; } map(fn: (value: T) => U): Option { return new None(); } andThen(fn: (value: T) => Option): Option { return none; } flatMap(fn: (value: T) => Option): Option { return new None(); } unwrap(): T { throw new Error("Tried to unwrap a non-existent value"); } unwrapOr(defaultValue: U): T | U { return defaultValue; } unwrapOrElse(fn: () => U): T | U { return fn(); } or(optb: Option): Option { return optb; } orElse(fn: () => Option): Option { return fn(); } match(some: (value: T) => A, none: () => B): A | B { return none(); } toString() { return `None`; } toJSON(): OptionJSON { return { tag: "none" }; } toNullable(): T | null { return null; } toBoolean(): boolean { return false; } okOrElse(errFn: () => E): Result { return err(errFn()); } } export type Option = Some | None; /** * Creates a new `Some` instance wrapping a `value`. * This function is a convenience method for creating `Some` values. * ```typescript * // Example * const a = some(5); // Some(5) * const b = some("foo") // Some("foo") * const c = none * * console.log(a) // Some { _tag: "Some", value: 5 } * console.log(b) // Some { _tag: "Some", value: "foo" } * console.log(c) // None { _tag: "None" } * * // Accessing the value stored inside: * const valueA = a.unwrap(); // 5 | unsafe method * const valueB = b.getOrElse("bar"); // "foo" | safe method * const valueC = c.getOrElse("bar"); // "bar" | safe method * * console.log(valueA) // 5 * console.log(valueB) // "foo" * console.log(valueC) // "bar" * * const unsafe = c.unwrap() // throws Error * ``` * @template `T` The type of the value being wrapped in the `Some`. * @param {T} `value` The value to wrap inside the `Some`. * @returns {Option} A new `Some` instance wrapping the provided value. */ export function some(value: T): Option; export function some(value: void): Option; export function some(value: T): Option { return new Some(value); } /** * A singleton representing a `None` instance. * This is used to represent the absence of a value and is often used as the default value for Option types. * ```typescript * // Example * const a = some(5); * const b = none; * * const valueA = a.unwrap() // 5 * const valueB = b.unwrap() // throws a error * * const valueA = a.getOrElse(10) // 5 * const valueB = a.getOrElse(10) // 10 * ``` */ export const none = new None(); export function fromNullableVal(value: T): Option> { if (!value) { return none; } return some(value); }