Keyborg/shared/utils/option.ts
2025-01-29 20:21:44 +03:00

390 lines
11 KiB
TypeScript

import { err, ok, Result } from "@shared/utils/result.ts";
type OptionJSON<T> = { tag: "some"; value: T } | { tag: "none" };
interface IOption<T> {
/**
* 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<T>;
ifSome(fn: (value: T) => void): Option<T>;
/**
* Checks if the `Option<T>` 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<T>;
ifNone(fn: () => void): Option<T>;
/**
* Uses the function `fn` to map a value of type `T`, stored inside of the `Option<T>`, to a value of type `U`. Then wraps a mapped value to the new `Option<U>` 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<U>} a new `Option<U>` wrapping the mapped value.
*/
map<U>(fn: (value: T) => U): Option<U>;
/**
* Uses the function `fn` to map a value of type `T`, stored inside of the `Option<T>`, to a value of type `Option<U>` 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<T>` and returns a new `Option<U>`.
* @returns {Option<U>} A new `Option<U>` wrapping the result of the flatMap operation.
*/
flatMap<U>(fn: (value: T) => Option<U>): Option<U>;
andThen<U>(fn: (value: T) => Option<U>): Option<U>;
/**
* **UNSAFE** method for extracting value `T` from an `Option<T>`.
* - If the `Option<T>` is `Some<T>` => returns value `T`
* - If the `Option<T>` 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<T>`.
* - If the `Option<T>` is `Some<T>` => returns value `T`
* - If the `Option<T>` 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<T>`, otherwise the `defaultValue`.
*/
unwrapOr<U>(defaultValue: U): T | U;
unwrapOrElse<U>(fn: () => U): T | U;
or<U>(optb: Option<U>): Option<T | U>;
orElse<U>(fn: () => Option<U>): Option<T | U>;
/**
* Matches on the `Option<T>` and applies the corresponding function for `Some<T>` or `None`.
* - If the `Option<T>` is `Some<T>` => 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<A, B = A>(some: (value: T) => A, none: () => B): A | B;
toNullable(): T | null;
toBoolean(): boolean;
okOrElse<E>(errFn: () => E): Result<T, E>;
toJSON(): OptionJSON<T>;
}
/**
* Represents a `Some<T>` value in the Option type, wrapping a value of type `T`.
* @template `T` The type of the value inside the `Some<T>`.
*/
export class Some<T> implements IOption<T> {
public readonly tag = "some";
/**
* Creates a new `Some<T>` instance.
* @param {T} The value `T` to wrap inside the `Some<T>`.
*/
constructor(public readonly value: T) {
Object.defineProperties(this, {
tag: {
writable: false,
enumerable: false,
},
});
}
isSome(): this is Some<T> {
return true;
}
ifSome(fn: (value: T) => void): this {
fn(this.value);
return this;
}
isNone(): this is None<T> {
return false;
}
ifNone(fn: () => void): Option<T> {
return this;
}
map<U>(fn: (value: T) => U): Option<U> {
return new Some(fn(this.value));
}
flatMap<U>(fn: (value: T) => Option<U>): Option<U> {
return fn(this.value);
}
andThen<U>(fn: (value: T) => Option<U>): Option<U> {
return fn(this.value);
}
unwrap(): T {
return this.value;
}
unwrapOr<U>(defaultValue: U): T | U {
return this.value;
}
unwrapOrElse<U>(fn: () => U): T | U {
return this.value;
}
or<U>(optb: Option<U>): Option<T | U> {
return this;
}
orElse<U>(fn: () => Option<U>): Option<T | U> {
return this;
}
match<A, B = A>(some: (value: T) => A, none: () => B): A | B {
return some(this.value);
}
toString() {
return `Some(${this.value})`;
}
toJSON(): OptionJSON<T> {
return { tag: "some", value: this.value };
}
toNullable(): T | null {
return this.value;
}
toBoolean(): boolean {
return true;
}
okOrElse<E>(errFn: () => E): Result<T, E> {
return ok<T, E>(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<T> implements IOption<T> {
public readonly tag = "none";
/**
* Creates a new `None` instance.
*/
constructor() {
Object.defineProperties(this, {
tag: {
writable: false,
enumerable: false,
},
});
}
isSome(): this is Some<T> {
return false;
}
ifSome(fn: (value: T) => void): Option<T> {
return this;
}
isNone(): this is None<T> {
return true;
}
ifNone(fn: () => void): Option<T> {
fn();
return this;
}
map<U>(fn: (value: T) => U): Option<U> {
return new None<U>();
}
andThen<U>(fn: (value: T) => Option<U>): Option<U> {
return none;
}
flatMap<U>(fn: (value: T) => Option<U>): Option<U> {
return new None<U>();
}
unwrap(): T {
throw new Error("Tried to unwrap a non-existent value");
}
unwrapOr<U>(defaultValue: U): T | U {
return defaultValue;
}
unwrapOrElse<U>(fn: () => U): T | U {
return fn();
}
or<U>(optb: Option<U>): Option<T | U> {
return optb;
}
orElse<U>(fn: () => Option<U>): Option<T | U> {
return fn();
}
match<A, B = A>(some: (value: T) => A, none: () => B): A | B {
return none();
}
toString() {
return `None`;
}
toJSON(): OptionJSON<T> {
return { tag: "none" };
}
toNullable(): T | null {
return null;
}
toBoolean(): boolean {
return false;
}
okOrElse<E>(errFn: () => E): Result<T, E> {
return err<T, E>(errFn());
}
}
export type Option<T> = Some<T> | None<T>;
/**
* 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<T>} A new `Some<T>` instance wrapping the provided value.
*/
export function some<T>(value: T): Option<T>;
export function some<T extends void = void>(value: void): Option<T>;
export function some<T>(value: T): Option<T> {
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<never>();
export function fromNullableVal<T>(value: T): Option<NonNullable<T>> {
if (!value) {
return none;
}
return some(value);
}