object/mapEntries.ts

/** @module Object */

import { isFunc } from '@method/isFunc'
import { set } from '@collection/set'
import { isArr } from '@array/isArr'
import { isObj } from './isObj'
import { isEntry } from './isEntry'

/**
 * Returns a new object, each entry of which is the result of applying the cb function to input's corresponding entry
 * @param {Object|Array} obj - regular object or array
 * @param {Function} cb  - function of form: (key, value) => [nextKey, nextValue]
 *  - the return type here is an array of two elements, key and value, where `key` must be either a string or a number
 *  - if a cb does not return an entry, then the original [key, value] pair that was passed into cb will be used instead
 * @example mapObj({a: 2, b: 3}, (k, v) => [k, v * v]) returns: {a: 4, b: 9}
 * @example mapObj({a: 1}, (k, v) => ['b', v]) returns: {b: 1}
 * @function
 *
 * @returns {Object} - new object with mapping applied, or the original obj if input was invalid
 */
export const mapEntries = <T = Record<string, any>>(
  obj: any | any[],
  cb: (key: string, value: any) => [string, any]
): T => {
  if (!isArr(obj) && !isObj(obj)) {
    console.error(obj, `Expected array or object for obj. Found ${typeof obj}`)
    return obj as T
  }

  if (!isFunc(cb)) {
    console.error(`Expected function for cb. Found ${typeof cb}`)
    return obj as T
  }

  const entries = Object.entries(obj)

  const initialValue = isArr(obj) ? [] : {}

  return entries.reduce((obj, [key, value]) => {
    const result = cb(key, value)
    if (!isEntry(result)) {
      console.error(
        `Callback function must return entry. Found: ${result}. Using current entry instead.`
      )
      return set(obj, key, value) as T
    }
    return set(obj, result[0], result[1]) as T
  }, initialValue as T)
}