ES2019: Object.fromEntries()

[2019-01-28] dev, javascript, es2019
(Ad, please don’t block)

The proposal “Object.fromEntries” (by Darien Maillet Valentine, Jordan Harband and Kevin Gibbons) is at stage 4 and therefore part of ECMAScript 2019. This blog post explains how it works.

Object.fromEntries() vs. Object.entries()  

Given an iterable over [key,value] pairs, Object.fromEntries() creates an object:

assert.deepEqual(
  Object.fromEntries([['foo',1], ['bar',2]]),
  {
    foo: 1,
    bar: 2,
  }
);

It does the opposite of Object.entries():

const obj = {
  foo: 1,
  bar: 2,
};
assert.deepEqual(
  Object.entries(obj),
  [['foo',1], ['bar',2]]
);

Combining Object.entries() with Object.fromEntries() helps with implementing a variety of operations related to objects. Read on for examples.

Examples  

In this section, we’ll use Object.entries() and Object.fromEntries() to implement several tool functions from the library Underscore.

_.pick(object, ...keys)  

pick() removes all properties from object whose keys are not among keys. The removal is non-destructive: pick() creates a modified copy and does not change the original. For example:

const address = {
  street: 'Evergreen Terrace',
  number: '742',
  city: 'Springfield',
  state: 'NT',
  zip: '49007',
};
assert.deepEqual(
  pick(address, 'street', 'number'),
  {
    street: 'Evergreen Terrace',
    number: '742',
  }
);

We can implement pick() as follows:

function pick(object, ...keys) {
  const filteredEntries = Object.entries(object)
    .filter(([key, _value]) => keys.includes(key));
  return Object.fromEntries(filteredEntries);
}

_.invert(object)  

invert() non-destructively swaps the keys and the values of an object:

assert.deepEqual(
  invert({a: 1, b: 2, c: 3}),
  {1: 'a', 2: 'b', 3: 'c'}
);

We can implement it like this:

function invert(object) {
  const mappedEntries = Object.entries(object)
    .map(([key, value]) => [value, key]);
  return Object.fromEntries(mappedEntries);
}

_.mapObject(object, iteratee, context?)  

mapObject() is like the Array method .map(), but for objects:

assert.deepEqual(
  mapObject({x: 7, y: 4}, value => value * 2),
  {x: 14, y: 8}
);

This is an implementation:

function mapObject(object, callback, thisValue) {
  const mappedEntries = Object.entries(object)
    .map(([key, value]) => {
      const mappedValue = callback.call(thisValue, value, key, object);
      return [key, mappedValue];
    });
  return Object.fromEntries(mappedEntries);
}

_.findKey(object, predicate, context?)  

findKey() returns the key of the first property for which predicate returns true:

const address = {
  street: 'Evergreen Terrace',
  number: '742',
  city: 'Springfield',
  state: 'NT',
  zip: '49007',
};
assert.equal(
  findKey(address, (value, _key) => value === 'NT'),
  'state'
);

We can implement it as follows:

function findKey(object, callback, thisValue) {
  for (const [key, value] of Object.entries(object)) {
    if (callback.call(thisValue, value, key, object)) {
      return key;
    }
  }
  return undefined;
}

An implementation  

Object.fromEntries() could be implemented as follows (I’ve omitted a few checks):

function fromEntries(iterable) {
  const result = {};
  for (const [key, value] of iterable) {
    let coercedKey;
    if (typeof key === 'string' || typeof key === 'symbol') {
      coercedKey = key;
    } else {
      coercedKey = String(key);
    }
    Object.defineProperty(result, coercedKey, {
      value,
      writable: true,
      enumerable: true,
      configurable: true,
    });
  }
  return result;
}

The official polyfill is available via the npm package object.fromentries.

A few more details about Object.fromEntries()  

  • Duplicate keys: If you mention the same key multiple times, the last mention “wins”.
    > Object.fromEntries([['a', 1], ['a', 2]])
    { a: 2 }
    
  • Symbols as keys: Even though Object.entries() ignores properties whose keys are symbols, Object.fromEntries() accepts symbols as keys.
  • Coercion of keys: The keys of the [key,value] pairs are coerced to property keys: Values other than strings and symbols are coerced to strings.
  • Iterables vs. Arrays:
    • Object.entries() returns an Array (which is consistent with Object.keys() etc.). Its [key,value] pairs are 2-element Arrays.
    • Object.fromEntries() is flexible: It accepts iterables (which includes Arrays and is consistent with new Map() etc.). Its [key,value] pairs are only required to be objects that have properties with the keys '0' and '1' (which includes 2-element Arrays).
  • Only enumerable data properties are supported: If you want to create non-enumerable properties and/or non-data properties, you need to use Object.defineProperty() or Object.defineProperties().