Source

accumulator.ts

/**
 * @class ObjectAccumulator
 * @template T - The type of the accumulated object, extends object
 * @description A class that accumulates objects and provides type-safe access to their properties.
 * It allows for dynamic addition of properties while maintaining type information.
 * @summary Accumulates objects and maintains type information for accumulated properties
 * @memberOf utils
 */
export class ObjectAccumulator<T extends object> {
  /**
   * @private
   * @description The size of the accumulated object
   * @type {number}
   */
  private __size!: number;

  constructor() {
    Object.defineProperty(this, "__size", {
      value: 0,
      writable: true,
      configurable: false,
      enumerable: false,
    });
  }

  /**
   * @protected
   * @description Expands the accumulator with properties from a new object
   * @summary Adds new properties to the accumulator
   * @template V - The type of the object being expanded
   * @param {V} value - The object to expand with
   * @returns {void}
   */
  protected expand<V extends object>(value: V): void {
    Object.entries(value).forEach(([k, v]) => {
      Object.defineProperty(this, k, {
        get: () => v,
        set: (val: V[keyof V]) => {
          v = val;
        },
        configurable: true,
        enumerable: true,
      });
    });
  }

  /**
   * @description Accumulates a new object into the accumulator
   * @summary Adds properties from a new object to the accumulator, maintaining type information
   * @template V - The type of the object being accumulated
   * @param {V} value - The object to accumulate
   * @returns A new ObjectAccumulator instance with updated type information
   * @mermaid
   * sequenceDiagram
   *   participant A as Accumulator
   *   participant O as Object
   *   A->>O: Get entries
   *   loop For each entry
   *     A->>A: Define property
   *   end
   *   A->>A: Update size
   *   A->>A: Return updated accumulator
   */
  accumulate<V extends object>(value: V): T & V & ObjectAccumulator<T & V> {
    this.expand(value);
    this.__size = this.__size + Object.keys(value).length;
    return this as unknown as T & V & ObjectAccumulator<T & V>;
  }

  /**
   * @description Retrieves a value from the accumulator by its key
   * @summary Gets a value from the accumulated object using a type-safe key
   * @template T - value type
   * @template K - The key type, must be a key of this
   * @param {K} key - The key of the value to retrieve
   * @returns The value associated with the key
   */
  get<K extends keyof T>(key: K): T[K] {
    if (!(key in this))
      throw new Error(
        `Key ${key as string} does not exist in accumulator. Available keys: ${this.keys().join(
          ", "
        )}`
      );
    return (this as any)[key as K] as T[K];
  }

  /**
   * @description Retrieves a value from the accumulator by its key
   * @summary Gets a value from the accumulated object using a type-safe key
   * @param {string} key - The key of the value to retrieve
   * @param {any} value - The key of the value to retrieve
   */
  put(key: string, value: any) {
    return this.accumulate({ [key]: value });
  }

  /**
   * @description Checks if a key exists in the accumulator
   * @summary Determines whether the accumulator contains a specific key
   * @param {string} key - The key to check for existence
   * @returns {boolean} True if the key exists, false otherwise
   */
  has(key: string): boolean {
    return !!this[key as keyof this];
  }

  /**
   * @description Removes a key-value pair from the accumulator
   * @summary Deletes a property from the accumulated object
   * @param {string} key - The key of the property to remove
   * @returns {} The accumulator instance with the specified property removed
   */
  remove(
    key: keyof this | string
  ):
    | (Omit<this, typeof key> & ObjectAccumulator<Omit<this, typeof key>>)
    | this {
    if (!(key in this)) return this;

    delete this[key as keyof this];
    this.__size--;
    return this as unknown as Omit<this, typeof key> &
      ObjectAccumulator<Omit<this, typeof key>>;
  }

  /**
   * @description Retrieves all keys from the accumulator
   * @summary Gets an array of all accumulated property keys
   * @returns {string[]} An array of keys as strings
   */
  keys(): string[] {
    return Object.keys(this);
  }

  /**
   * @description Retrieves all values from the accumulator
   * @summary Gets an array of all accumulated property values
   * @returns An array of values
   */
  values(): T[keyof T][] {
    return Object.values(this);
  }

  /**
   * @description Gets the number of key-value pairs in the accumulator
   * @summary Returns the count of accumulated properties
   * @returns {number} The number of key-value pairs
   */
  size(): number {
    return this.__size;
  }

  /**
   * @description Clears all accumulated key-value pairs
   * @summary Removes all properties from the accumulator and returns a new empty instance
   * @returns {ObjectAccumulator<never>} A new empty ObjectAccumulator instance
   */
  clear(): ObjectAccumulator<never> {
    return new ObjectAccumulator();
  }

  /**
   * @description Executes a callback for each key-value pair in the accumulator
   * @summary Iterates over all accumulated properties, calling a function for each
   * @param {function(any, string, number): void} callback - The function to execute for each entry
   * @returns {void}
   */
  forEach(
    callback: (value: this[keyof this], key: keyof this, i: number) => void
  ): void {
    Object.entries(this).forEach(([key, value], i) =>
      callback(value, key as keyof this, i)
    );
  }

  /**
   * @description Creates a new array with the results of calling a provided function on every element in the accumulator
   * @summary Maps each accumulated property to a new value using a callback function
   * @template R - The type of the mapped values
   * @param {function(any, string,number): R} callback - Function that produces an element of the new array
   * @returns {R[]} A new array with each element being the result of the callback function
   */
  map<R>(
    callback: (value: this[keyof this], key: keyof this, i: number) => R
  ): R[] {
    return Object.entries(this).map(([key, value], i) =>
      callback(value, key as keyof this, i)
    );
  }
}