pery mimon
pery mimon

Reputation: 8315

equivalent of _.pick() but for array in Lo-dash

I have the following collection:

var columns = [
    { key:'url', width:20, type:'text' },
    { key:'title', width:21, type:'text' },
    { key:'desc', width:22, type:'text' },
    { key:'domain', width:23, type:'text' },
    { key:'user', width:24, type:'text' }
];

I'm looking for a method to map an array of objects with picked keys, something along the lines of:

_.mapPick(columns, [width]) 
// [{width:20},{width:21},{width:22},{width:23},{width:24}]

I know I can extend lo-dash like this:

_.mixin({
    mapPick:  mapPick:function (objs,keys){
        return _.map(objs, function (obj) {
            return _.pick(obj,keys)
        })
    }

});

I'm not sure if there is some native function that I'm missing.

I found a similar question here but I'm looking for a more lo-dash native way.

Upvotes: 49

Views: 41700

Answers (8)

SuSuSoo
SuSuSoo

Reputation: 11

you can use Curry

var columns = [
    { key:'url', width:20, type:'text' },
    { key:'title', width:21, type:'text' },
    { key:'desc', width:22, type:'text' },
    { key:'domain', width:23, type:'text' },
    { key:'user', width:24, type:'text' }
];

result = _.map(columns, _.curry(_.pick, 2)(_, ['width']));

out:

[ 
  { width: 20 },
  { width: 21 },
  { width: 22 },
  { width: 23 },
  { width: 24 } 
]

Upvotes: 1

Almas Kamitov
Almas Kamitov

Reputation: 103

Why not just using a pick here (~50kb import)?

npm i lodash.pick

Solution:

const pick = require('lodash.pick')

const filterKeys = ['width']
const columns = [
    { key: 'url'.  , width: 20, type: 'text' },
    { key: 'title' , width: 21, type: 'text' },
    { key: 'desc'  , width: 22, type: 'text' },
    { key: 'domain', width: 23, type: 'text' },
    { key: 'user'  , width: 24, type: 'text' },
]

const result = columns.map(item => pick(item, filterKeys))

Output:

[ 
  { width: 20 },
  { width: 21 },
  { width: 22 },
  { width: 23 },
  { width: 24 } 
]

Upvotes: 1

Jacopo Tedeschi
Jacopo Tedeschi

Reputation: 149

Short Answer

I looked extensively into the docs, and it would seem that currently in Lodash 4.17.15 (as of now 26th Jan 2021) no such functionality is directly implemented in the library. Accordingly:

_.map(columns, row => _.pick(row, 'key'))

//Output, when using the columns collection I introduce below:
//[{key: "url"},
// {key: "user"},
// {}]

shall be considered the preferred solution. If the key is not found:

  • the key will simply not be attached to the object
  • you will get back an empty object

Long Answer

I believe it may be worth noting what advantages and disadvantages may lay in the alternative approaches. Let's have a tour between them, considering the following collection, designed to show some drawbacks that may arise from each solution:

var columns = [
    { 0: 'pos_url', key:'url', width:20, type:'text' },
    { 1: 'pos_user', key:'user', width:24, type:'text' },
    "key"
];

Try all the approaches explained here on Codepen if you like!

It may be a little messy, but I actually wrote a small codepen were you can make your hands dirty and try all it's explained here, feel free to try it:

https://codepen.io/jhack_jos/pen/gOwVReB

You have to open your browser console to see the console.log outputs.

1) Using the "project" function

If like me, you actually do not want to use mixins and mess with Lodash, you could also make use of the following function I created, called project. It is inspired by Michael Fogus "Functional JavaScript", as he introduces a similar function in its talk about "Table-Like" Data and "Applicative Programming". The name comes from the unary operation called projection in relational algebra, which does something very similar.

with ES6

function project(...properties)
{
  return function project(object, index, array)
    {
      return _.pick(object, ...properties);
    };
}

without ES6

function project(/* properties here */)
{
  var properties = arguments;
  return function project(object, index, array)
    {
      return _.pick.bind(null, object).apply(null, properties);
    };
}

Some examples of usage:

_.map(columns, project("key"));

//Output:
//[{key: "url"},
// {key: "user"},
// {}]

_.map(columns, project("key", "width"));

//Output:
//[{key: "url", width: 20},
// {key: "user", width: 24},
// {}]

_.map(columns, project("key", ["width", "type"]));

//Output:
//[{key: "url", width: 20, type: "text"},
// {key: "user", width: 24, type: "text"},
// {}]

How fast are the different implementations of project?

  • the ES6 implementation is always the fastest
  • without ES6, on V8 the bind + apply combination is the fastest implementation I found. On SpiderMonkey, even if by a small margin, using Array.prototype.concat is faster. You can find such implementation in the link below.
  • link to the tests on jsbench.me, also containing other possible implementations of project: https://jsbench.me/2gkkfl9pw4

If the key is not found:

  • the key will simply not be attached to the object
  • you will get back an empty object

2) The _.mapPick approach

I actually really like the mixin approach used by @pery mimon in the question. It is also possible to use _.chain with it, which makes the code very readable. Just keep in mind the possibility of clashing with future Lodash updates. Unfortunately, the algorithm as it is does not behave as one may expect when it is thrown a list of properties as arguments (it ignores all but the first argument). I may suggest the following improvement:

with ES6

_.mixin({
    mapPick:  function (objs, ...keys) {
        return _.map(objs, function (obj) {
            return _.pick(obj, ...keys);
        })
    }
});

without ES6

_.mixin({
    mapPick:  function (objs) {
        var properties = arguments;
        return _.map(objs, function (obj) {
            return _.pick.bind(null, obj).apply(null, properties);
        })
    }
});

Thanks to this we can now do:

_.mapPick(columns, "key", "width")

//Output now:
//[{key: "url", width: 20}
// {key: "user", width: 24}
// {}]

//Output of the original mixin from the question:
//[{key: "url"}
// {key: "user"}
// {}]

If the key is not found:

  • the key will simply not be attached to the object
  • you will get back an empty object

3) The _.partialRight approach

This approach is not recommended, as it may reveal to be tricky if incorrectly applied in edge case scenarios. Lodash docs states _.partialRight does the following:

This method is like _.partial except that partially applied arguments are appended to the arguments it receives.

This means that _.partialRight(func, arg1, arg2) (arg3, arg4) resolves into func(arg3, arg4, arg1, arg2).

The wrong way to do it

As such, the following code:

_.map(columns, _.partialRight(_.pick, 'key'));

Resolves into:

_.map(columns, (row, index, columns) => _.pick(row, index, columns, 'key'));

//Output:
//[{0: "pos_url", key: "url"},
// {1: "pos_user", key: "user"},
// {2: "y"}]

If the key is not found:

  • the key will simply not be attached to the object
  • you will get back an empty object

The right way to do it

You should therefore use _.unary to restrict the arguments passed down to _.pick to just one. Code is as follows:

_.map(columns, _.unary(_.partialRight(_.pick, 'key')));

This resolves to:

_.map(columns, (row, index, columns) => _.pick(row, 'key'));

//Output:
//[{key: "url"},
// {key: "user"},
// {}]

If the key is not found:

  • the key will simply not be attached to the object
  • you will get back an empty object

4) Destructuring and arrow functions

You may expect this approach to cover everything and be vanilla, but you may find it slightly inflexible in edge cases. Let's have a look at it:

columns.map(({key}) => ({key}))

//Output:
//[{key: "url"},
// {key: "user"},
// {key: undefined}]

A few relevant facts:

  • you cannot pick numerical keys
  • you cannot pick keys that contain spaces

If the key is not found:

  • you will get a key with value "undefined"
  • you will never get an empty object
  • _.keys will still return the key that was not found

5) Extracting values without keys

This one is an extra, and does not comply with the question. As another answer pointed out, in case you do not need the keys, you may just extract the values with the _.map shortcut (previously known as _.pluck):

_.map(columns, "key")

//Output:
//["url", "user", undefined]

There are a lot of interesting things to know about this way of doing it, as it implicitly makes use of the _.iteratee function, which enables us to make such a brilliant use of _.get with _.map .

If the key is not found:

  • undefined will be used as a value
  • you will get an array of the same length as the original, but filled with "undefined" values

Upvotes: 6

alexloehr
alexloehr

Reputation: 1831

With ES6 destructuring and arrow functions you can do it like this:

columns.map(({width}) => ({width}))

No lodash required.

Upvotes: 10

Dawid
Dawid

Reputation: 774

I found simpler solution in map examples:

var users = [
    {name:'David',age:35},
    {name:'Kate',age:28},
];
_.map(users,'age'); // [35,28]

Upvotes: 19

VladOhotnikov
VladOhotnikov

Reputation: 1188

In my case I also need cast to type, and problem was what TS compiler represents Adam Boduch solution return a boolean, so I find a little simple way to do this:

_.map(columns, item => { return { key: item.key } });

In this case you also can add some new properties.

P.S. Because I cant post a comment I will add explanation about partialRight usage here:

_.map(columns, _.partialRight(_.pick, 'key'));

First if u call _.map(array, func) func will be called for every array element. So it's equal: _.map(array, item => func(item)).

Second the result of call partialRight will be a function newFunc, so we can represent Adam's code as:

var newFunc = _.partialRight(_.pick, 'key');
_.map(array, item => newFunc(item));

Third we can represent the newFunc logic as:

function newFunc(item) {
    return _.pick(item, 'key')
}

Finally I think most understandable and readable solution for this problem is:

_.map(columns, item => _.pick(item, 'key'))

Upvotes: 26

Metalstorm
Metalstorm

Reputation: 3232

If you only want one of the keys you can do:

_.map(collection, 'key')

Your example:

var columns = [
    { key:'url', width:20, type:'text' },
    { key:'title', width:21, type:'text' },
    { key:'desc', width:22, type:'text' },
    { key:'domain', width:23, type:'text' },
    { key:'user', width:24, type:'text' }
];

var widths = _.map(columns, 'width');

widths
[20, 21, 22, 23, 24]

Upvotes: 4

Adam Boduch
Adam Boduch

Reputation: 11211

I think the map() + pick() approach is your best bet. You could compose the callback instead of creating an inline function however:

_.map(columns, _.partialRight(_.pick, 'key'));

Upvotes: 36

Related Questions