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
| Check | Own properties | Inherited | Key present but undefined |
|---|---|---|---|
Object.hasOwn(o, k) | yes | no | reports present |
k in o | yes | yes | reports present |
o[k] !== undefined | yes | yes | reports missing |
o.hasOwnProperty(k) | yes | no | reports 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 }
| Need | Pick |
|---|---|
| fixed, known field names (a record) | object |
| keys that come from data, or any key type | Map |
| strict insertion order including numeric keys | Map |
| a size you read often, or frequent add and delete | Map |
| JSON serialisation without conversion | object |
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.keyslists own enumerable keys only, so it sees justb. Theinoperator walks the prototype chain, so it finds the inheriteda. 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 nofiltermethod, so you borrow the array ones: entries out, array method,fromEntriesback in. Afor...inloop works too but also walks inherited keys.- When is a
Mapthe better choice than an object? - When the keys come from data, may not be strings, or must keep strict insertion order — A
Mapaccepts any key type, keeps insertion order for every key including numbers, exposessize, and has no prototype keys to collide with. Objects still win for fixed record shapes and direct JSON support.
Common mistakes
- Using
obj[key] !== undefinedto test presence, then losing keys that legitimately holdundefined. - Reaching for
for...inon data objects and picking up inherited keys. - Accumulating counts on a plain
{}when keys come from user data, soconstructorandtoStringcorrupt the result.
Takeaways
keys,valuesandentriessee own enumerable string keys only.Object.fromEntriescloses the loop so array methods can transform objects.Object.hasOwnis the modern presence check,inincludes inherited members.- When keys come from data, consider a
MaporObject.create(null).