26

https://lodash.com/docs#mapKeys

Is it possible to map an Object's keys deeply using Lodash? If not, is there another library providing this functionality (if grouped with other deep iteration and manipulation functionality, even better!)? Else, how would one go about implementing this? The main struggle I see is in identifying pure key/value objects that are safely, deeply iterable. It's easy to throw out Arrays, but it's important to note that the function shouldn't try to deeply iterate on other objects like a regex, for example.

Intended result-

var obj = { a: 2, b: { c: 2, d: { a: 3 } } };
_.deepMapKeys(obj, function (val, key) {
  return key + '_hi';
});
// => { a_hi: 2, b_hi: { c_hi: 2, d_hi: { a_hi: 3 } } }
Machavity
  • 30,841
  • 27
  • 92
  • 100
Tejas Manohar
  • 954
  • 2
  • 11
  • 28

7 Answers7

39

Here's how you can do that in lodash:

_.mixin({
    'deepMapKeys': function (obj, fn) {

        var x = {};

        _.forOwn(obj, function(v, k) {
            if(_.isPlainObject(v))
                v = _.deepMapKeys(v, fn);
            x[fn(v, k)] = v;
        });

        return x;
    }
});

and here's a more abstract mixin, that recursively applies any given mapper:

_.mixin({
    deep: function (obj, mapper) {
        return mapper(_.mapValues(obj, function (v) {
            return _.isPlainObject(v) ? _.deep(v, mapper) : v;
        }));
    },
});

Usage (returns the same as above):

obj = _.deep(obj, function(x) {
    return _.mapKeys(x, function (val, key) {
        return key + '_hi';
    });
});

Another option, with more elegant syntax:

_.mixin({
    deeply: function (map) {
        return function(obj, fn) {
            return map(_.mapValues(obj, function (v) {
                return _.isPlainObject(v) ? _.deeply(map)(v, fn) : v;
            }), fn);
        }
    },
});


obj = _.deeply(_.mapKeys)(obj, function (val, key) {
    return key + '_hi';
});
georg
  • 211,518
  • 52
  • 313
  • 390
  • This is a great solution so I'll upvote it. However, I find the accepted answer simpler at solving the option so that's the approach I'd recommend. – Tejas Manohar Jan 28 '16 at 09:17
  • @TejasManohar: well, you asked for code that is lodash based and is able to distinguish between plain and typed objects... the accepted answer is neither. – georg Jan 28 '16 at 09:28
  • 2
    Nice! I needed a version of `deeply()` that digs into Arrays as well. Here it is: https://gist.github.com/zambon/8b2d207bd21cf4fcd47b96cd6d7f99c2 – Henrique Zambon Mar 22 '17 at 13:15
  • How do you use it in typescript? How to type this mixin? `Property 'deeply' does not exist on type 'typeof import("/node_modules/@types/lodash-es/index")'.` – Magnus Pääru Mar 23 '22 at 14:15
  • @Magnus Pääru, [here it is](https://stackoverflow.com/a/75010148/12468111) – FrameMuse Jan 04 '23 at 19:13
7

Not a best one as per performance efficiency, but If you want to skip recursion part from your side and want to keep the code clean and simple then you can stringify it to json and use JSON.parse with additional parameter (callback) and there you can transform the keys with lodash.

JSON.parse(JSON.stringify(obj), (k, v) => _.isObject(v) ? _.mapKeys(v, (_v, _k) => _k + '_hi') : v)

Here is an working example:

let obj = { a: 2, b: { c: 2, d: { a: 3 } } };
let mappedObj = JSON.parse(JSON.stringify(obj), (k, v) => _.isObject(v) ? _.mapKeys(v, (_v, _k) => _k + '_hi') : v)
console.log(mappedObj)
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.10/lodash.min.js"></script>
Koushik Chatterjee
  • 4,106
  • 3
  • 18
  • 32
5

In extention of georg's answer, here's what I'm using. This extended mixin adds the ability to map arrays of objects within the object too, a simple but important change.

_.mixin({
    deeply: function (map) {
      return function (obj, fn) {
        return map(_.mapValues(obj, function (v) {
          return _.isPlainObject(v) ? _.deeply(map)(v, fn) : _.isArray(v) ? v.map(function(x) {
            return _.deeply(map)(x, fn);
          }) : v;
        }), fn);
      }
    },
  });
GTWelsh
  • 93
  • 2
  • 7
2

EDIT: This will map the objects values, not its keys. I misunderstood the question.


function deepMap (obj, cb) {
    var out = {};

    Object.keys(obj).forEach(function (k) {
      var val;

      if (obj[k] !== null && typeof obj[k] === 'object') {
        val = deepMap(obj[k], cb);
      } else {
        val = cb(obj[k], k);
      }

      out[k] = val;
    });

  return out;
}

And use it as

var lol = deepMap(test, function (v, k) {
    return v + '_hi';
});

It will recursively iterate over an objects own enumerable properties and call a CB passing it the current value and key. The values returned will replace the existing value for the new object. It doesn't mutate the original object.

See fiddle: https://jsfiddle.net/1veppve1/

ste2425
  • 4,656
  • 2
  • 22
  • 37
  • 2
    It's important to note that this returns a new object with different values, not keys- like asked in the question. That said, it's a simple change, and it expresses conceptual understanding so I'll save a downvote :) – Tejas Manohar Jan 28 '16 at 09:26
  • @TejasManohar Damn yes sorry I misunderstood the point. I thought your were mapping the value not keys. Ill add an edit so others don't get confused. – ste2425 Jan 28 '16 at 09:38
  • @alexanderElgin I know it involves effort but would you mind updating my fiddle to show a way with less redundant code? I don't mean that to sound sarcastic, I'm interested in improving my knowledge and all that. – ste2425 Jan 28 '16 at 10:25
1

I've add a small improvement from Chris Jones's answer.

Following code fix some unintended results of Array elements.

_.mixin({
  deeply: function (map) {
    var deeplyArray = function (obj, fn) {
      return obj.map(function(x) {
        return _.isPlainObject(x) ? _.deeply(map)(x, fn) : x;
      })
    }

    return function (obj, fn) {
      if (_.isArray(obj)) {
        return deeplyArray(obj, fn);
      }

      return map(_.mapValues(obj, function (v) {
        return _.isPlainObject(v) ? _.deeply(map)(v, fn) : _.isArray(v) ? 
          deeplyArray(v, fn) : v;
      }), fn);
    }
  },
});
Sunho Hong
  • 527
  • 4
  • 3
0

Accepted solution does not work with deeper arrays/obj/simple-types or arrays of simple types(strings/numbers) like

{
  "notification-type": 1,
  "reciept_my": {
    "latest__info": [
      {
        "a-a": 1,
        "reciept_my": { "latest__info": [{ "a-a": 1 }, "b", true, "d"] }
      },
      "b",
      true,
      "d"
    ]
  }
}

For example ['s'] is converted to Object {0: "s"}

Here is an improvement to make it work:

function deepMapKeys(obj, fn) {
  if (!_.isArray(obj) && !_.isPlainObject(obj)) {
    return obj;
  }

  const x = _.isArray(obj) ? [] : {};

  _.forOwn(obj, function (v, k) {
    if (_.isArray(v)) {
      v = v.map((v) => deepMapKeys(v, fn));
    }
    if (_.isPlainObject(v)) {
      v = deepMapKeys(v, fn);
    }
    x[fn(k)] = v;
  });

  return x;
}
0

Typescript

This is a typed version of georg's answer.

Be careful, if you're not importing this directly, it will throw "deeply is not a function" error - you need to import (or write) this mixin before your code calls _.deeply.

import _, { Dictionary, ObjectIterator } from "lodash"

declare module "lodash" {
  interface LoDashStatic {
    deeply<T extends object, TResult>(map: unknown): (obj: T | null | undefined, callback: ObjectIterator<T, TResult>) => { [P in keyof T]: TResult }
  }
}

_.mixin({
  deeply: (map) => {
    return (object: Dictionary<unknown>, callback: ObjectIterator<object, unknown>) => {
      return map(_.mapValues(object, value => {
        return _.isPlainObject(value) ? _.deeply(map)(value as object, callback) : value
      }), callback)
    }
  },
})

export default {}

My implementation

/**
 * https://stackoverflow.com/questions/38304401/javascript-check-if-dictionary/71975382#71975382
 */
export function isDictionary(object: unknown): object is Record<keyof never, unknown> {
  return object instanceof Object && object.constructor === Object
}

/**
 * https://stackoverflow.com/a/75010148/12468111
 */
export function mapKeysDeep(object: Record<keyof never, unknown>, callback: (key: string, value: unknown) => keyof never): Record<string, unknown> {
  function iterate(value: unknown): unknown {
    if (isDictionary(value)) {
      return mapKeysDeep(value, callback)
    }

    if (value instanceof Array) {
      return value.map(iterate)
    }

    return value
  }

  return (
    Object
      .entries(object)
      .reduce((result, [key, value]) => ({ ...result, [callback(key, value)]: iterate(value) }), {})
  )
}

Don't forget to put links to code that you copy-pasted (please).

FrameMuse
  • 143
  • 1
  • 9