Object Utilities

Mental model: Turn an object into entries, use array methods, turn it back. That covers most object work.

Level: intermediate · about 11 minutes

const stock = { apples: 3, pears: 0, figs: 7 };

console.log(Object.keys(stock));    // ['apples', 'pears', 'figs']
console.log(Object.values(stock));  // [3, 0, 7]
console.log(Object.entries(stock)); // [['apples', 3], ['pears', 0], ['figs', 7]]

Three views of the same object.

These three functions return arrays of the own enumerable string-keyed properties. Inherited properties and symbol keys are left out, which is almost always what you want.

The round trip: entries in, object out

const stock = { apples: 3, pears: 0, figs: 7 };

const inStock = Object.fromEntries(
  Object.entries(stock).filter(([, count]) => count > 0)
);
console.log(inStock); // { apples: 3, figs: 7 }

const doubled = Object.fromEntries(Object.entries(stock).map(([k, v]) => [k, v * 2]));
console.log(doubled); // { apples: 6, pears: 0, figs: 14 }

Filter and map an object by borrowing array methods.

Three ways to ask "is it there?"

const settings = { theme: 'dark', lang: undefined };

console.log(Object.hasOwn(settings, 'lang'));   // true, the key exists
console.log(settings.lang !== undefined);       // false, the value is undefined
console.log('toString' in settings);            // true, inherited from the prototype
console.log(Object.hasOwn(settings, 'toString'));// false, not its own
CheckOwn propertiesInheritedKey present but undefined
Object.hasOwn(o, k)yesnoreports present
k in oyesyesreports present
o[k] !== undefinedyesyesreports missing
o.hasOwnProperty(k)yesnoreports present, but breaks on null-prototype objects

Grouping

const people = [
  { name: 'Ada', team: 'eng' },
  { name: 'Grace', team: 'eng' },
  { name: 'Katherine', team: 'maths' },
];

const byTeam = Object.groupBy(people, (p) => p.team);
console.log(Object.keys(byTeam));   // ['eng', 'maths']
console.log(byTeam.eng.length);     // 2

Iteration order, and the loop that surprises people

const parent = { inherited: true };
const child = Object.create(parent);
child.own = 1;

for (const key in child) console.log('for-in sees:', key); // own, inherited
console.log(Object.keys(child));                            // ['own']

Prefer for (const [k, v] of Object.entries(obj)). It only sees own enumerable properties, it gives you the value without a second lookup, and it follows the same key order rules as Object.keys.

Object or Map?

const scores = { ada: 10, grace: 9 };

const asMap = new Map(Object.entries(scores));
asMap.set('katherine', 8);
console.log(asMap.get('ada'), asMap.size);   // 10 3

const backToObject = Object.fromEntries(asMap);
console.log(backToObject); // { ada: 10, grace: 9, katherine: 8 }
NeedPick
fixed, known field names (a record)object
keys that come from data, or any key typeMap
strict insertion order including numeric keysMap
a size you read often, or frequent add and deleteMap
JSON serialisation without conversionobject
const o = { a: undefined };
console.log(Object.hasOwn(o, 'a'), 'a' in o, o.a !== undefined);

The key exists, so both Object.hasOwn and in report true. Only the value comparison says "missing", because the stored value really is undefined. Pick the check that matches the question you are asking.

Try it yourself

Object in, object out

const prices = { widget: 9.99, gizmo: 0, doohickey: 24.5 };

const affordable = Object.fromEntries(
  Object.entries(prices)
    .filter(([, price]) => price > 0 && price < 20)
    .map(([name, price]) => [name, Math.round(price)])
);

console.log(affordable);
console.log('gizmo present?', Object.hasOwn(prices, 'gizmo'), 'truthy?', Boolean(prices.gizmo));

Add a step that renames every key to upper case. Then swap the pipeline to use a Map and compare.

Exercises

Invert an object

Write invert(obj) which swaps keys and values: { a: "1" } becomes { "1": "a" }. Use Object.entries and Object.fromEntries. If two keys share a value, the last one wins. Do not modify the input.

Count by a key function

Write countBy(items, fn) which calls fn(item) to get a group key and returns an object mapping each key to how many items produced it. It must handle key names that clash with inherited properties, such as "constructor".

Check yourself

What does this log?
1 true — Object.keys lists own enumerable keys only, so it sees just b. The in operator walks the prototype chain, so it finds the inherited a. That difference is the whole point of having both.
You need to filter an object down to the entries whose value is truthy. What is the idiomatic route?
Object.fromEntries(Object.entries(obj).filter(...)) — Objects have no filter method, so you borrow the array ones: entries out, array method, fromEntries back in. A for...in loop works too but also walks inherited keys.
When is a Map the better choice than an object?
When the keys come from data, may not be strings, or must keep strict insertion order — A Map accepts any key type, keeps insertion order for every key including numbers, exposes size, and has no prototype keys to collide with. Objects still win for fixed record shapes and direct JSON support.

Common mistakes

  • Using obj[key] !== undefined to test presence, then losing keys that legitimately hold undefined.
  • Reaching for for...in on data objects and picking up inherited keys.
  • Accumulating counts on a plain {} when keys come from user data, so constructor and toString corrupt the result.

Takeaways

  • keys, values and entries see own enumerable string keys only.
  • Object.fromEntries closes the loop so array methods can transform objects.
  • Object.hasOwn is the modern presence check, in includes inherited members.
  • When keys come from data, consider a Map or Object.create(null).