UNPKG

nochoices

Version:

Full featured implementation of options into typescript.

893 lines (847 loc) 28.4 kB
import { OptionalValue } from "./optional-value.js" import { Some } from "./some.js" import { None } from "./none.js" import { AreEqual, FlattenOption, GenerateOption, Generator, Predicate, Transformation, TransformToOption, ZipTransformation } from "./types.js"; /** * * An Option<T> represents a value of type T that can be present ot not. * Values inside options cannot be used directly, which ensures a safe * data consumption. * * There are several ways to create an optional value: * * @example * ```ts * const none = Option.None() * const some = Option.Some('foo') * const nullable: string | null = 'bar' * const maybe = Option.fromNullable(nullable) * ``` * * An optional can also be created combining other optionals: * * @example * ```ts * const opt1 = Option.None() * const opt2 = Option.Some('foo') * const opt3 = opt1.or(opt2) * ``` * * Optional values can also perform operations * * @example * ```ts * const opt = Option.Some('foo') * const opt2 = opt.map(v => v + 'bar') // === Some('foobar') * const opt3 = opt.filter(v => v.length === 0) // === None * ``` * * @param T - The type of the object wrapped by the optional */ export class Option<T> { /** * @hidden * @private */ private value: OptionalValue<T> /** * @hidden * @param value - Internal value for the optional * @private */ private constructor(value: OptionalValue<T>) { this.value = value } /** * Creates an instance of Option with a value. (Some) * * @param value - The value to be wrapped in an Option. * @typeParam T - Type of the value that the Option may contain. * @returns An instance of Option containing the provided value ( Some(value) ). * * @example * ```ts * const some = Option.Some('foo') * some.unwrap() === 'foo' // true * ``` */ static Some<T>(value: T): Option<T> { return new Option(new Some(value)) } /** * Creates an empty optional value (represents no value). * * @typeParam T - Type of the value that the Option may contain. * @returns An instance of Option without a value ( None() ). * * @example * ```ts * const none = Option.None() * none.isNone() // true * ``` */ static None<T>(): Option<T> { return new Option<T>(new None()) } /** * Creates an instance of Option from a nullable value. * If the provided value is null or undefined, it returns an Option without a value (None). * Otherwise, it wraps the provided value in an Option (Some). * * @param param - The value to be wrapped in an Option. * @typeParam T - Type of the value that the Option may contain. * @returns An instance of Option containing the provided value if it's not null or undefined, otherwise an Option without a value. * * @example * ```ts * const nullable: string | null = null * const maybe = Option.fromNullable(nullable) // None * const nullable2: string | null = 'foo' * const maybe2 = Option.fromNullable(nullable2) // Some('foo') * ``` */ static fromNullable<T>(param: T | null | undefined): Option<T> { if (param === null || param === undefined) { return Option.None() } else { return Option.Some(param) } } /** * Returns true if the instance does not contain a value. Returns false otherwise. * * @returns true if instance is none, otherwise true * * @example * ```ts * const none = Option.None() * none.isNone() // true * const some = Option.Some('foo') * some.isNone() // false * ``` */ isNone(): boolean { return this.value.isAbsent() } /** * Checks if the Option instance contains a value. * * @returns Returns true if the Option instance contains a value, otherwise false. * * @example * ```ts * const none = Option.None() * none.isSome() // false * const some = Option.Some('foo') * some.isSome() // true * ``` */ isSome(): boolean { return this.value.isPresent() } /** * Transforms the value contained in the Option instance using the provided mapping function. * If the Option instance does not contain a value (None), it returns a new Option without a value. * If the Option instance contains a value (Some), it applies the mapping function to the value and returns a new * option with the mapped value. * * @param fn - The mapping function to apply to the value. * @typeParam M - The type of the value that the new Option may contain after applying the mapping function. * @returns A new Option with the mapped value. * * @example * ```ts * const some = Option.Some(5) * const newSome = some.map(value => value * 2) // Some(10) * const none = Option.None<number>() * const newNone = none.map(value => value * 2) // None * ``` */ map<M>(fn: Transformation<T, M>): Option<M> { return this.value.map(fn) } /** * Unwraps the value contained in the Option instance. * If the Option instance does not contain a value (None), it throws an error. * If the Option instance contains a value (Some), it returns the value. * * This method is better suited for testing or inspection. * Use it with care. There are safer alternatives to this operation * like {@link Option.unwrapOr | `unwrapOr`}, {@link Option.unwrapOrElse | `unwrapOrElse`}, * {@link Option.getOrInsert | `getOrInsert`}, {@link Option.getOrInsertWith | `getOrInsertWith`}, * {@link Option.take | `take`} or {@link Option.takeIf | `takeIf`}. * * If the desire is throw an error in case of a missing value, `expect` it's a better * alternative that allows for more expressive errors. * * @returns The value contained in the Option instance. * @throws If the Option instance does not contain a value. * * @example * ```ts * const some = Option.Some('foo') * some.unwrap() // 'foo' * const none = Option.None() * none.unwrap() // throws Error * ``` */ unwrap(): T { return this.value.unwrap() } /** * Unwraps the value contained in the Option instance or returns a default value if the Option * instance does not contain a value. * This is a safer alternative to `unwrap` where the normal flow of the program can be ensured. * * @param defaultValue - Value returned when current instance is None. * @returns The value contained in the Option instance or the provided default value. * * @example * ```ts * const some = Option.Some('foo') * some.unwrapOr('bar') // 'foo' * const none = Option.None() * none.unwrapOr('bar') // 'bar' * ``` */ unwrapOr<U>(defaultValue: U): T | U { return this.value.unwrapOr(defaultValue) } /** * Returns the contained value. If there is no value it executes the provided function and * returns the result of that. * This is a safer alternative to plain `unwrap` that ensures that the flow of the program * can continue in case there is a missing value. * * @param defaultFn - When instance is None, this function is called to create * a default value. * @returns The value contained in the intance or the generated default value. * * @example * ```ts * const some = Option.Some('foo') * some.unwrapOrElse(() => 'bar') // 'foo' * const none = Option.None() * none.unwrapOrElse(() => 'bar') // 'bar' * ``` */ unwrapOrElse(defaultFn: Generator<T>): T { return this.value.unwrapOrElse(defaultFn) } /** * Filters the value contained in the Option instance using the provided predicate function. * * - If the Option instance does not contain a value (None), it returns a new Option without a * value. * - If the Option instance contains a value (Some) and the predicate function returns true when * applied to the value, it returns a new Option with the same value. * - If the Option instance contains a value (Some) and the predicate function returns false when * applied to the value, it returns a new Option without a value (None). * * @param fn - The predicate function used to filter. * @returns A new Option with the value if the predicate function returns true, * otherwise an Option without a value. * * @example * ```ts * const opt1 = Option.Some(5) * const newOpt1 = opt1.filter(value => value > 3) // Some(5) * const opt2 = Option.None<number>() * const newOpt2 = opt2.filter(value => value > 3) // None * ``` */ filter(fn: Predicate<T>): Option<T> { return this.value.filter(fn) } /** * If there is a value present returns it, otherwise throws the error specified as argument. * * This is the right method to use when you an error should be raised if the optional is empty. * * @param err - The error to throw if the Option instance does not contain a value. * @returns The value contained in the Option instance. * @throws The provided error if the Option instance does not contain a value. * * @example * ```ts * const some = Option.Some('foo') * some.expect(new Error('No value')) // 'foo' * const none = Option.None() * none.expect(new Error('No value')) // throws Error: 'No value' * ``` */ expect(err: Error): T { return this.value.expect(err) } /** * Flattens nested options. * An `Option<Option<T>>` returns an `Option<T>` with the same value inside (or no value in case of None). * * In case the option is not nested, it returns the same option. * * There is a type safer alternative to this method as an exported function {@link flatten} * * @returns Flatterned version of the option. * * @example * ```ts * const some = Option.Some(Option.Some('foo')) * some.flatten() // Some('foo') * const none = Option.None() * none.flatten() // None * ``` */ flatten(): Option<FlattenOption<T>> { return this.value.flatten() } /** * Returns the value inside the optional after applying the given transformation. If the * optional is empty it returns the default value * * @param defaultValue - The default value to return if the Option instance does not contain a value. * @param mapFn - The mapping function to apply to the value. * @returns The transformed contained value, or the provided default. * * @example * ```ts * const some = Option.Some(5) * const result = some.mapOr(0, value => value * 2) // 10 * const none = Option.None<number>() * const result2 = none.mapOr(0, value => value * 2) // 0 * ``` */ mapOr<U>(defaultValue: U, mapFn: Transformation<T, U>): U { return this.map(mapFn).unwrapOr(defaultValue) } /** * Returns the value inside the optional after applying the given transformation. If the * optional is empty it execs the generator function and returns the result of it. * * The generator function is not called if there is a value. * * @param defFn - The default value generation function to call if the Option instance * does not contain a value. * @param mapFn - The mapping function to apply to the value. * @returns The transformed value, or the default generated value. * * @example * ```ts * const some = Option.Some(5) * const result = some.mapOrElse(() => 0, value => value * 2) // 10 * const none = Option.None<number>() * const result2 = none.mapOrElse(() => 0, value => value * 2) // 0 * ``` */ mapOrElse<U>(defFn: () => U, mapFn: Transformation<T, U>): U { return this.map(mapFn).unwrapOrElse(defFn) } /** * Combines 2 options into an option with a tuple of size 2 inside. * In case tha any of the options (this, or the argument) is none, the result is going to be none. * * @param another - The other Option instance to combine with. * @typeParam U - The type of the value that the other Option may contain. * @returns A new Option typed with a tuple of the 2 values. * * @example * ```ts * const some1 = Option.Some('foo') * const some2 = Option.Some(5) * const result = some1.zip(some2) // Some(['foo', 5]) * const none = Option.None() * const result2 = some1.zip(none) // None * ``` */ zip<U>(another: Option<U>): Option<[T, U]> { return this.value.zip(another.value) } /** * Combines 2 options and then applies a transformation. The result is an option * types as the result of the transformation. If any of the original options * is empty the result will be empty. * * @param another - The other Option instance to combine with. * @param zipWithFn - The transformation function to apply to the * values of both Option instances. * @typeParam U - The type of the value that the other Option may contain. * @typeParam V - The type returned by the transformation. * @returns A new Option containing the result of applying the transformation * function to both values if both Option instances contain a value, * otherwise an Option without a value. * * @example * ```ts * const some1 = Option.Some('foo') * const some2 = Option.Some(5) * const result = some1.zipWith(some2, (a, b) => a + b) // Some('foo5') * const none = Option.None() * const result2 = some1.zipWith(none, (a, b) => a + b) // None * ``` */ zipWith<U, V>(another: Option<U>, zipWithFn: ZipTransformation<T, U, V>): Option<V> { return this.zip(another).map(([t, u]) => zipWithFn(t, u)) } /** * Returns a new option that is only present if both values (this, and the argument) * are present. The value returned is the value contained in the option received as argument. * * This method behaves similar to the `&&` operator, but translated to optional values instead * of booleans. * * @param another - Another optional value. * @typeParam V - The type of the value that the other Option may contain. * @returns The result of applying an `and` operation between the 2 optionals. * * @example * ```ts * const some = Option.Some('foo') * const another = Option.Some(5) * const result = some.and(another) // Some(5) * const none = Option.None() * const result2 = none.and(another) // None * const result3 = some.and(none) // None * ``` */ and<V>(another: Option<V>): Option<V> { return this.value.and(another) } /** * Returns a new Option instance that is only present if any of the instances * (this, and the argument) are present. * If `this` is present it returns the value contained in `this`. Otherwise returns * the value contained in the argument * * This operation behaves similar to the `||` but adapted from booleans to options. * * @param another - The other Option instance to combine with. * @returns A new option only present if any is present * * @example * ```ts * const some = Option.Some('foo') * const another = Option.Some('bar') * const result = some.or(another) // Some('foo') * const none = Option.None() * const result2 = none.or(another) // Some('bar') * const result3 = some.or(none) // Some('foo') * const result4 = none.or(none) // None * ``` */ or(another: Option<T>): Option<T> { return this.value.or(this, another) } /** * Returns a new Option instance that is only present if exactly one of the instances * (this, and the argument) are present. The value is the value of the present optional * or None if both are None. * * This operation behaves similar to the `xor` (exclusive or) operation but * adapted from booleans to options. * * @param another - The other Option instance to combine with. * @returns A new Option instance that is only present if exactly one of the instances (this, and the argument) are present. * * @example * ```ts * const some = Option.Some('foo') * const another = Option.Some('bar') * const result = some.xor(another) // None * const none = Option.None() * const result2 = none.xor(another) // Some('bar') * const result3 = some.xor(none) // Some('foo') * const result4 = none.xor(none) // None * ``` */ xor(another: Option<T>): Option<T> { return this.value.xor(another.value) } /** * Similar to {@link Option.and | `and` }, but allowing the second optional to be generated * lazily. The provided fn is not executed if `this` is None. * * @param fn - Function to generate the second option. It takes the * content of the current instance as argument. * @typeParam U - The type returned by the given function. * @returns A new option only present if this is Some and the result * of the function is Some. The value is the one returned by the function. * * @example * ```ts * const some = Option.Some(5) * const newSome = some.andThen(value => Option.Some(value * 2)) // Some(10) * const none = Option.None<number>() * const newNone = none.andThen(value => Option.Some(value * 2)) // None * ``` */ andThen<U>(fn: TransformToOption<T, U>): Option<U> { return this.value.andThen(fn) } /** * Similar to {@link Option.or | `or`} but allowing to generate the second option * lazily. The generator fn will only be called if needed. * * @param fn - function to generate an optional value to test agains * `this`. * @returns A new option only present if any of the options is present. * * @example * ```ts * const some = Option.Some('foo') * const result = some.orElse(() => Option.Some('bar')) // Some('foo') * const none = Option.None() * const result2 = none.orElse(() => Option.Some('bar')) // Some('bar') * const result3 = none.orElse(() => Option.None()) // None * ``` */ orElse(fn: GenerateOption<T>): Option<T> { return this.value.orElse(fn) } /** * Inserts a value into the Option instance, replacing the current value if it exists. * * @param value - The value to be inserted into the Option instance. * @returns The Option instance itself. * * @example * ```ts * const none = Option.None() * none.insert('foo') // Some('foo') * const some = Option.Some('bar') * some.insert('foo') // Some('foo') * ``` */ insert(value: T): Option<T> { this.value = new Some(value) return this } /** * If the instance contains a value returns that value. * If the instance is empty it inserts the value provided by argument, and returns the value. * * Notice that if the instance already had a value this method does not replace it and the * argument is ignored. * * @param value - The value to be inserted and returned if the instance is None. * @returns The value contained in the Option instance after the operation. * * @example * ```ts * const opt1 = Option.None() * opt1.getOrInsert('foo') // 'foo' * opt1.isPresent() // true * * const opt2 = Option.Some('bar') * opt2.getOrInsert('foo') // 'bar' * ``` */ getOrInsert(value: T): T { this.value = this.value.getOrInsert(value) return this.unwrap() } /** * Similar to {@link Option.insert | `insert` } but allowing the inserted value to be calculated * lazily. * * If the instance is Some the value inside the option is present and the generator * function is ignored. * * If the instance is None, the provided function is called, and the result is inserted * into the instance and returned. * * @param fn - Function to generate the value to insert and return in case of none. * @returns The value contained in the Option instance after the operation. * * @example * ```ts * const opt1 = Option.None() * opt1.getOrInsertWith(() => 'foo') // 'foo' * opt1.isPresent() // true * * const opt2 = Option.Some('bar') * opt2.getOrInsertWith(() => 'foo') // 'bar' * ``` */ getOrInsertWith(fn: Generator<T>): T { this.value = this.value.getOrInsertWith(fn) return this.unwrap() } /** * Takes the value inside the instance and returned inside a new Option. * * If the instance is Some, it gets transformed into None * * If the instance is None, it gets unnafected and theresult is None. * * @returns A new Option instance containing the value originally * contained in the Option instance. * * @example * ```ts * const opt1 = Option.Some('foo') * const taken = opt1.take() // Some('foo') * opt1.isNone() // true * const none = Option.None() * const takenFromNone = none.take() // None * none.isNone() // true * ``` */ take(): Option<T> { const takeValue = this.value.takeValue() this.value = new None() return takeValue } /** * Replaces the contained value of an option with the one provided as argument. * * @param newValue - The new value to be inserted into the Option instance. * @returns A new Option instance containing the old value. * * @example * ```ts * const opt1 = Option.Some('foo') * const oldSome = opt1.replace('bar') // Some('foo') * opt1.unwrap() // 'bar' * const opt2 = Option.None() * const oldNone = opt2.replace('foo') // None * opt2.unwrap() // 'foo' * ``` */ replace(newValue: T): Option<T> { const oldValue = this.value this.value = new Some(newValue) return new Option<T>(oldValue) } /** * Returns true if and only if the current instance is Some and the value fulfills the given * predicate. * * This is the options equivalent to do something like the following but in the world * of optionals. * * ```ts * if (a && myCondition(a)) { * //... * } * ``` * * @param andFn - The predicate function to apply to the contained value. * @returns Returns true if the Option instance contains a value and the predicate * function returns true when applied to the value, otherwise false. * * @example * ```ts * const some = Option.Some(5) * const result = some.isSomeAnd(value => value > 3) // true * const none = Option.None<number>() * const result2 = none.isSomeAnd(value => value > 3) // false * ``` */ isSomeAnd(andFn: Predicate<T>): boolean { return this.value.isSomeAnd(andFn) } /** * Returns true if and only if the current instance is Some and the value does not fulfill the given * predicate. * * This is the options equivalent to do something like the following but in the world * of optionals. * * ```ts * if (a && !myCondition(a)) { * //... * } * ``` * * @param condition - The predicate function to apply to the contained value. * @returns Returns true if the Option instance contains a value and the predicate * function returns false when applied to the value, otherwise true. * * @example * ```ts * const some = Option.Some(5) * const result = some.isSomeBut(value => value > 3) // false * const none = Option.None<number>() * const result2 = none.isSomeBut(value => value > 3) // false * ``` */ isSomeBut(condition: Predicate<T>) { return this.value.isSomeAnd((t) => !condition(t)) } /** * Allows to execute a block of code only if the instance is Some. It always * returns the current instance. * * It also allows to expressive chains like this: * * ```ts * let opt: Option<number> * opt.ifSome((value) => { * console.log(`value: ${value}`) * }).orElse(() => { * console.log('no value') * }) * ``` * * @param fn - Function executed if instance is some. * @returns Itself. * * @example * ```ts * const some = Option.Some('foo') * const res1 = some.ifSome(value => console.log(value)) // logs 'foo' to the console * res1 === some // true * const none = Option.None() * const res2 = none.ifSome(value => console.log(value)) // does nothing * res2 === none // true * ``` */ ifSome(fn: (t: T) => void): Option<T> { this.value.ifSome(fn) return this } /** * Executes the provided function if the Option instance does not contain a value. * It always returns itself. * * It's a nice method to chain with others * * ```ts * const a: Option<number> * * a.ifNone(() => { * // ... do something * }).andThen((value) => { * // ... only executed if the first block was not. * }) * ``` * * @param fn - The function to execute if the Option instance does not contain a value. * @returns The Option instance itself. * * @example * ```ts * const some = Option.Some('foo') * some.ifNone(() => console.log('No value')) // does nothing * const none = Option.None() * none.ifNone(() => console.log('No value')) // logs 'No value' to the console * ``` */ ifNone(fn: () => void): Option<T> { this.value.ifNone(fn) return this } /** * Useful to evaluate what's inside the optional. * If the instance is Some it executes the block of code with the value as argument. * No mutability is done. * Nonthing is returned. * * If the instance is None, nothing happens. * * @param param - Function to exec if there is value inside. * * @example * ```ts * const some = Option.Some('foo') * some.inspect(value => console.log(value)) // logs 'foo' to the console * const none = Option.None() * none.inspect(value => console.log(value)) // does nothing * ``` */ inspectContent(param: (t: T) => void) { this.ifSome(param) } /** * If the instance is None returns None. * If the value inside the instnace pass the predicate, the instance gets transformed to None * and the value is returned * * @param param - The predicate function to apply to the value. * @returns A new option with the filtered value. * * @example * ```ts * const opt1 = Option.Some(5) * const result1 = opt1.takeIf(value => value > 3) // Some(5) * opt1.isNone() // true * const opt2 = Option.Some(2) * const result2 = opt2.takeIf(value => value > 3) // Some(5) * opt2.isSome() // true * opt2.unwrap() // 2 * const opt3 = Option.None<number>() * const result3 = opt3.takeIf(value => value > 3) // None * opt3.isNone() // true * ``` */ takeIf(param: Predicate<T>): Option<T> { return this.filter(param).andThen(() => this.take()) } /** * Converts the Option instance to an array. * If the instance is None it returns an empty array * If the instance is Some it returns an array of size 1 containing the value. * * @returns An array with the content of the option. * * @example * ```ts * const some = Option.Some('foo') * some.toArray() // ['foo'] * const none = Option.None() * none.toArray() // [] * ``` */ toArray(): T[] { return this.value.toArray() } /** * Returns true if and only of both optionals are some and both have the same * value. Comparison is done using `===`. * * @param another - Another optional to compare with this * * @example * ```ts * Option.None().equals(Option.None()) // true * Option.Some(10).equals(Option.None()) // false * Option.None().equals(Option.Some('foo')) // false * Option.Some('bar').equals(Option.Some('bar')) // true * ``` */ equals(another: Option<T>): boolean { return this.value.equalsWith(another.value, (a, b) => a === b) } /** * Returns true if and only of both optionals are some and the provided * equality check returns true. * * @param another - Another optional to compare with this * @param equality - Function to compare content of both. * * @example * ```ts * // If both are None, the equality function is not even called * Option.None().equalsWith(Option.None(), () => false) // true * * // If one is some and the other is none, the equality function is not even called * Option.Some(10).equalsWith(Option.None(), () => true) // false * Option.None().equalsWith(Option.Some('foo'), () => true) // false * * // If both are some the equality function is called * Option.Some(10).equalsWith(Option.Some(15), (a, b) => a % 5 === b % 5) // true * Option.Some(7).equalsWith(Option.Some(15), (a, b) => a % 5 === b % 5) // false * ``` */ equalsWith(another: Option<T>, equality: AreEqual<T>): boolean { return this.value.equalsWith(another.value, equality) } }