390 lines
11 KiB
TypeScript
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);
|
|
}
|