Chris
Chris

Reputation: 659

Mapping an object to array of objects in ES6

How would I convert a object to an array of objects while keeping key names?

// actual 
obj = {
  key1: null,
  key2: "Nelly",
  key3: [ "suit", "sweat" ]
} 

// expected 
arr = [
  { key2: "Nelly" },
  { key3: [ "suit", "sweat" ] }
]

currently my solution is...

 var arr = Object.keys(obj).map(key => { if (obj[key]) return { key: obj[key] } });

which returns

arr = [
  undefined,
  { key: "Nelly" },
  { key: [ "suit", "sweat" ] }
]

Upvotes: 6

Views: 9553

Answers (4)

Mulan
Mulan

Reputation: 135416

Transducers

There's heaps of answers here to help you reach your answer in a practical way – filter this, map that, and voilà, the result you're looking for. There's other answers using primitive for loops, but those make you sad.

So you're wondering, "is it possible to filter and map without iterating through the array more than once?" Yes, using transducers.


Runnable demo

I might update this paragraph with more code explanation if necessary. ES6 comin' at you …

// Trans monoid
const Trans = f => ({
  runTrans: f,
  concat: ({runTrans: g}) =>
    Trans(k => f(g(k)))
})

Trans.empty = () =>
  Trans(k => k)

const transduce = (t, m, i) =>
  i.reduce(t.runTrans((acc, x) => acc.concat(x)), m.empty())

// complete Array monoid implementation
Array.empty = () => []

// transducers
const mapper = f =>
  Trans(k => (acc, x) => k(acc, f(x)))
  
const filterer = f =>
  Trans(k => (acc, x) => f(x) ? k(acc, x) : acc)
  
const logger = label =>
  Trans(k => (acc, x) => (console.log(label, x), k(acc, x)))

// your function, implemented with transducers  
const foo = o => {
  const t = logger('filtering')
    .concat(filterer(k => o[k] !== null))
    .concat(logger('mapping'))
    .concat(mapper(k => ({ [k]: o[k] })))
    .concat(logger('result'))
  return transduce(t, Array, Object.keys(o))
}

console.log(foo({a: null, b: 2, c: 3}))

Output; notice the steps appear interlaced – filtering, mapping, result, repeat – this means each of the combined transducers run for each iteration of the input array. Also notice how because a's value is null, there is no mapping or result step for a; it skips right to filtering b – all of this means we only stepped thru the array once.

// filtering a
// filtering b
// mapping b
// result { b: 2 }
// filtering c
// mapping c
// result { c: 3 }
// => [ { b: 2 }, { c: 3 } ]

Finishing up

Of course that foo function has lots of console.log stuff tho. In case it's not obvious, we just want to remove the logger transducers for our actual implementation

const foo = o => {
  const t = filterer(k => o[k] !== null)
    .concat(mapper(k => ({ [k]: o[k] })))
  return transduce(t, Array, Object.keys(o))
}

console.log(foo({a: null, b: 2, c: 3}))
// => [ {b: 2}, {c: 3} ]

Attribution

My enlightenment on the subject is owed exclusively to Brian Lonsdorf and accompanying work: Monoidal Contravariant Functors Are Actually Useful

Upvotes: 7

nnnnnn
nnnnnn

Reputation: 150070

.map() returns an array of the same length as the original array. Code like yours with a callback that doesn't return a value in some cases will result in elements with the value undefined. One way to deal with that is to first .filter() out the elements you don't want to keep.

Anyway, to get the key names you want you can use an object literal with a computed property name:

{ [key]: obj[key] }

In context:

const obj = {
  key1: null,
  key2: 'Nelly',
  key3: [ 'suit', 'sweat' ]
}

const arr = Object.keys(obj)
  .filter(v => obj[v] != null)
  .map(key => ({ [key]: obj[key] }))

console.log(arr)

Upvotes: 14

Mμ.
Mμ.

Reputation: 8552

If you use map, the length of your expected array will be the same as the number of keys in your input. So map is not appropriate in this case. My solution is to use a reduce function like so:

var obj = {
  key1: null,
  key2: 'Nelly',
  key3: [ 'suit', 'sweat' ]
} 

var res = Object.keys(obj).reduce(
  (acc, curr) => {
    // if current key's value is not null
    // insert object to the resulting array acc
    if (obj[curr])  { 
      acc.push({[curr] : obj[curr]}); 
      return acc; 
    }
    // if they key value is null, skip it
    return acc; 
}, [] );

console.log(res);

Upvotes: 1

Will Reese
Will Reese

Reputation: 2841

As @zerkms says, I don't think using multiple es6 functions is going to improve your code. Try a loop!

// actual 
let obj = {
  key1: null,
  key2: "Nelly",
  key3: [ "suit", "sweat" ]
};

let arr = [];
let k = Object.keys(obj);

for(let i = 0, len = k.length; i < len; i++) {
  let key = k[i];
  if (obj[key]) {
    arr.push({key: obj[key]});
  }
}

Upvotes: 3

Related Questions