TOP

JSDoc - "Result"

As mentioned, I don't use TypeScript, but write many of my apps in pure Javascript. As such, in order to describe types, I use JSDoc extensively in many of my codebases.
Hoping someone would find it interesting, I have a several pages presenting JSDoc examples as to how you could effectively utilize JSDoc's classical doctrine syntax.
Here is one of the modules that I currently use on my website, specifically, the Result module.
I was inspired by Rust's Result<T, E> type.
/**
 * `Result` is a monad representing either
 * a success ('Ok') or a failure ('Err').<br />
 * Inspired by Rust's `Result<T, E>` type.
 * ```
 * const { Result } = await import('./Result.js');
 * const r1 = Result().ok(42);
 * console.log(r1.unwrap()); // 42
 *
 * const r2 = Result().err(new Error('fail'));
 * console.log(r2.unwrap_or('default')); // "default"
 *
 * // map / flatMap
 * const r3 = Result(2).map(function (x) { return x * 3; }); // Ok(6)
 * const r4 = Result(2).flatMap(function (x) { return Result(x + 5); }); // Ok(7)
 *
 * // match pattern
 * r3.match({
 *   Ok: function (v) { console.log('Ok:', v); },
 *   Err: function (e) { console.error('Err:', e); }
 * });
 * ```
 * There are more fancy methods available:
 * ```
 * try {
 *   result.unwrap_or_throw();
 * } catch(err) {
 *   console.log(err);
 * }
 * ```
 *
 * This module also has definitions for `ResultEntity` and `ResultInterface`.<br />
 * However, while they are both defined in the same file, JSDoc generates<br />
 * separate pages for `ResultEntity` and `ResultInterface`.
 * @module lib/Result
 * @see {@link module:lib/Result~ResultInterface|ResultInterface} - Although defined in the same file, JSDoc generates a separate page
 */

/**
 * __ResultEntity (instance created)__<br />
 * `ResultEntity` is a monad holding `Ok` value or `Err`.<br />
 * To create an instance, run `Result` factory.
 * @typedef ResultEntity
 * @implements module:lib/Result~ResultInterface<T,E>
 * @template T, E
 * @property {function(T):ResultEntity<T,E>} ok - Marks this Result as Ok.
 * @property {function(E):ResultEntity<T,E>} err - Marks this Result as Err.
 * @property {function():boolean} is_ok - Returns true if Ok.
 * @property {function():boolean} is_err - Returns true if Err.
 * @property {function():T} unwrap - Returns Ok or throws if Err.
 * @property {function(*):*} unwrap_or - Returns Ok or fallback.
 * @property {function():E} unwrap_err - Returns the Err value.
 * @property {function():void} throw_if_err - Throws if Err.
 * @property {function():T} unwrap_or_throw - Throws if Err, else returns Ok.
 * @property {ResultMapper<T,U,E>} map - Maps Ok through a function.
 * @property {ResultMapper<T,U,E>} flatMap - Maps and flattens nested Result.
 * @property {ResultMatcher<T,E>} match - Pattern matches on Ok/Err.
 * @see {@link module:lib/Result~ResultInterface|ResultInterface} - For the interface of "ResultEntity"
 */

/**
 * A mapper function that transforms the `Ok` value.
 * @callback ResultMapper
 * @template T,U,E
 * @param {T} value
 * @returns {ResultEntity<U,E>}
 */

/**
 * A function for pattern matching on `Result` values.
 * @callback ResultMatcher
 * @template T,E
 * @param {{Ok:function(T):any, Err:function(E):any}} pattern
 * @returns {any}
 */

/**
 * __ResultInterface (interface)__<br />
 * Interface design for `ResultEntity`.
 * @interface ResultInterface
 * @template T, E
 * @see {@link module:lib/Result~ResultEntity|ResultEntity} - Although defined in the same file, generates 2 separate pages.
 */

/**
 * __Result (factory)__<br />
 * A factory function to create `ResultEntity`.<br />
 * @function
 * @name Result
 * @template T, E
 * @param {T} [value=] - If given optionally, it will set the given value as `Ok` value, and will set the monad `Ok`.
 * @returns {ResultEntity<T,E>}
 * @see {@link module:lib/Result~ResultEntity|ResultEntity} - See definitions of the returned "ResultEntity".
 * @see {@link module:lib/Result~ResultInterface|ResultInterface} - For the interface of "ResultEntity".
 */
export const Result = value => {
  /**
   * @type {ResultEntity<T,E>}
   */
  const self = {};

  /**
   * @type {boolean}
   */
  let _ok = false;

  /**
   * @type {?E}
   */
  let _err = null;

  /**
   * @type {?T}
   */
  let _value = null;

  __init(value);

  /**
   * For the given value, sets it as `Ok` value, and sets the monad `Ok`.
   * @function
   * @method module:lib/Result~ResultInterface#ok
   * @param {T} value
   * @returns {ResultEntity<T,E>}
   */
  self.ok = value => {
    _ok = true;
    _err = null;
    _value = value;
    return Object.freeze(self);
  };

  /**
   * For the given value, sets it as `Err` value, and sets the monad `Err`.
   * @function
   * @method module:lib/Result~ResultInterface#err
   * @param {E} err
   * @returns {ResultEntity<T,E>}
   */
  self.err = err => {
    _ok = false;
    _err = err;
    _value = null;
    return Object.freeze(self);
  };

  /**
   * Checks if the monad is `Err`.
   * @function
   * @method module:lib/Result~ResultInterface#is_err
   * @returns {boolean}
   */
  self.is_err = () => !_ok;

  /**
   * Checks if the monad is `Ok`.
   * @function
   * @method module:lib/Result~ResultInterface#is_ok
   * @returns {boolean}
   */
  self.is_ok = () => _ok;

  /**
   * Returns the `Ok` value or throws if `Err`.
   * @function
   * @method module:lib/Result~ResultInterface#unwrap
   * @throws {Error}
   * @returns {T}
   */
  self.unwrap = () => {
    if (!_ok) {
      const message =
        typeof _err?.message === 'string'
          ? _err.message
          : '';
      throw new Error(
        `unwrap() called on Err: ${message}`
      );
    }
    return _value;
  };

  /**
   * Returns `Ok` or a fallback value.
   * @function
   * @method module:lib/Result~ResultInterface#unwrap_or
   * @template U
   * @param {U} given
   * @returns {T | U}
   */
  self.unwrap_or = given => (!!_ok ? _value : given);

  /**
   * Regardless of whether the monad being `Ok` or `Err`, returns the internally held `Err`.
   * @function
   * @method module:lib/Result~ResultInterface#unwrap_err
   * @returns {E}
   */
  self.unwrap_err = () => _err;

  /**
   * Throws the `Err` if present.
   * @function
   * @method module:lib/Result~ResultInterface#throw_if_err
   * @throws {E}
   */
  self.throw_if_err = () => {
    if (!_ok) throw _err;
  };

  /**
   * Returns `Ok` or throws the `Err`.
   * @function
   * @method module:lib/Result~ResultInterface#unwrap_or_throw
   * @throws {*}
   * @returns {T}
   */
  self.unwrap_or_throw = () => {
    self.throw_if_err();
    return self.unwrap();
  };

  /**
   * Maps the `Ok` value through a function `function(T):U`.
   * @method module:lib/Result~ResultInterface#map
   * @template U
   * @function
   * @param {function(T):U} fn - Mapping function `function(T):U` for transforming `Ok` values.
   * @returns {ResultEntity<U,E>}
   * @see {@link module:lib/Result~ResultMapper|ResultMapper}
   */
  self.map = function (fn) {
    if (_ok) {
      try {
        return Result(fn(_value));
      } catch (err) {
        return Result().err(/** @type {E} */ (err));
      }
    }
    return Result().err(_err);
  };

  /**
   * Maps and flattens nested `Result`s `function(T):ResultEntity<U,E>`.
   * @method module:lib/Result~ResultInterface#flatMap
   * @template U
   * @function
   * @param {function(T):ResultEntity<U,E>} fn - Mapping function `function(T):ResultEntity<U,E>` returning another `Result`.
   * @returns {ResultEntity<U,E>}
   * @see {@link module:lib/Result~ResultMapper|ResultMapper}
   */
  self.flatMap = function (fn) {
    if (_ok) {
      try {
        const next = fn(_value);
        if (
          next &&
          typeof next.is_ok === 'function'
        ) {
          return next;
        }
        return Result().err(
          /** @type {E} */ (
            new Error(
              'flatMap did not return a Result'
            )
          )
        );
      } catch (err) {
        return Result().err(/** @type {E} */ (err));
      }
    }
    return Result().err(_err);
  };

  /**
   * Pattern matches on `Ok` or `Err` values<br />
   * `function({Ok:function(T):any, Err:function(E):any}):any`.
   * @method module:lib/Result~ResultInterface#match
   * @param {{Ok:function(T):any, Err:function(E):any}} pattern - Pattern matcher `function({Ok:function(T):any, Err:function(E):any}):any`.
   * @see {@link module:lib/Result~ResultMatcher|ResultMatcher}
   * @returns {any}
   */
  self.match = function (pattern) {
    if (_ok) {
      return pattern.Ok(_value);
    }
    return pattern.Err(_err);
  };

  function __init(value) {
    if (typeof value !== 'undefined') {
      self.ok(value);
    }
  }

  return Object.freeze(self);
};
TOP