index.ts

export type OfTypeFunc<T> = () => T;

class Maybe<T = unknown> {
  private readonly __value: T;

  /**
   * Private constructor. Use <code>Maybe.of()</code> instead.
   * @constructor
   */
  private constructor(val: T) {
    this.__value = val;
  }

  /**
   * Public constructor. Creates an instance of Maybe.
   * @param val {*} Object, string, number, or function (direct object access)
   * @example const exampleObj = {
   *      foo: 'bar',
   *      baz: [1,2,3]
   * };
   *
   * const maybe1 = Maybe.of(exampleObj);
   * const maybe2 = Maybe.of(() => exampleObj.baz.1);
   * @returns {Maybe} A Maybe monad
   */
  static of<T>(val: unknown | OfTypeFunc<T>): Maybe {
    try {
      return new Maybe(typeof val === 'function' ? val() : val);
    } catch (error) {
      return new Maybe(undefined);
    }
  }

  /**
   * Get the monad's value
   * @example const maybe1 = Maybe.of(123);
   * const maybe2 = Maybe.of(null);
   *
   * maybe1.join();   // 123
   * maybe2.join();   // null
   * @returns {*} Returns the value of the monad
   */
  join(): T {
    return this.__value;
  }

  /**
   * Determine whether the monad's value exists
   * @example const maybe1 = Maybe.of(123);
   * const maybe2 = Maybe.of(undefined);
   *
   * maybe1.isJust();    // true
   * maybe2.isJust();    // false
   * @returns {boolean} <code>true</code> if the value is defined,
   * <code>false</code> if the monad is null or undefined.
   */
  isJust(): boolean {
    return !this.isNothing();
  }

  /**
   * Determine whether the monad's value is null or undefined
   * @example const maybe1 = Maybe.of(null);
   * const maybe2 = Maybe.of(123);
   *
   * maybe1.isNothing();    // true
   * maybe2.isNothing()     // false
   * @returns {boolean} <code>true</code> if the value is null or
   * undefined, <code>false</code> if the value is defined.
   */
  isNothing(): boolean {
    return this.__value === null || this.__value === undefined;
  }

  /**
   * Chain to the end of a monad as the default value to return if the <code>isNothing()</code> is true
   * @param defaultValue {string} Return this value when
   * <code>join()</code> is called and <code>isNothing()</code> is true
   * @example const maybe1 = Maybe.of(null);
   *
   * maybe1.orElse('N/A');
   * maybe1.join();   // 'N/A'
   * @returns {Maybe} A monad containing the default value
   */
  orElse(defaultValue: unknown): Maybe {
    if (this.isNothing()) {
      return Maybe.of(defaultValue);
    }
    return this;
  }

  /**
   * Apply a transformation to the monad
   * @param transform {function} The transformation function to apply to the monad
   * @example Maybe.of(1).map(val => val + 1);
   * @returns {Maybe} A monad created from the result of the transformation
   */
  map(transform: (val: T) => T | Maybe<T>): Maybe {
    if (typeof transform !== 'function') throw new Error('transform must be a function');
    if (this.isNothing()) {
      return Maybe.of(undefined);
    }
    return Maybe.of(transform(this.join()));
  }

  /**
   * Chain together functions that return Maybe monads
   * @param chain {function} Function that is passed the value of the calling monad, and returns a monad.
   * @example function addOne (val) {
   *   return Maybe.of(val + 1);
   * }
   *
   * const three = Maybe.of(1)
   *  .chain(addOne)
   *  .chain(addOne)
   *  .join();
   * @returns {Maybe} A monad created from the result of the transformation
   */
  chain(chain: (val: T) => Maybe<T>): Maybe {
    if (typeof chain !== 'function') throw new Error('chain must be a function');
    return this.map(chain).join() as Maybe
  }
}

export default Maybe;