Array.prototype.map builds a new array by running every element through a callback. There is no built-in equivalent for objects, so implement one. objectMap(obj, mapper) returns a new object with the same keys as obj, but every value replaced by mapper(value, key, obj). The original object is left untouched, just like Array.prototype.map never mutates the array it walks.
// obj: Record<string, V> — the source object to read.
// mapper: (value: V, key: string, obj) => U
// — called once per OWN key; its return value becomes the new value.
// returns: Record<string, U> — a new object, same keys, mapped values.
function objectMap(obj, mapper): Record<string, U>;
// Double every value.
objectMap({ a: 1, b: 2 }, (v) => v * 2);
// → { a: 2, b: 4 }
// The mapper also receives the key, so values can be built from both.
objectMap({ a: 1, b: 2 }, (v, k) => k + v);
// → { a: 'a1', b: 'b2' }
obj; only the values change. Values may become any type.mapper(value, key, obj) — mirroring Array.prototype.map's (element, index, array). The third argument is the original object.obj exactly as it was.objectMap({}, fn) returns {}.You'll build a new object that keeps the same keys as the input but runs each value through a callback first — Array.prototype.map, except for an object's values.
Say you have a price list: { apple: 1, pear: 2 }. You want the same list with tax added to every price. You don't want to touch the original — you want a fresh object, same product names, new numbers. objectMap is that operation generalized: hand it an object and a mapper, and it gives back a new object with identical keys whose values are whatever mapper returns for each one. The mapper is called as mapper(value, key, obj), the same shape as the callback you already know from Array.prototype.map.
Picture each key/value pair walking through the mapper one at a time. The key rides along unchanged; only the value is transformed. Whatever the mapper returns gets parked under the same key in a brand-new object. When every pair has passed through, that new object is your answer. The input is only ever read, never written.
The obvious move is to loop the object's keys and overwrite each value in place:
function objectMap(obj, mapper) {
for (const key in obj) {
obj[key] = mapper(obj[key], key, obj); // mutates the caller's object!
}
return obj;
}
This has two real bugs. First, it mutates the input — obj[key] = ... writes straight back into the caller's object, so after the call their original data is gone. Second, for...in doesn't stop at the object's own keys — it walks inherited enumerable keys up the prototype chain too. If the object was made with Object.create({ inherited: 1 }), the loop visits inherited as well, mapping a key that was never the object's own.
function objectMap(obj, mapper) {
// Start a FRESH object so the caller's input is never touched.
const result = {};
// Object.keys returns ONLY the object's own enumerable keys — inherited
// prototype keys are skipped, which is exactly the spec we want.
for (const key of Object.keys(obj)) {
// Call the mapper with the same (value, key, obj) shape as Array.map,
// and park its return value under the SAME key in the new object.
result[key] = mapper(obj[key], key, obj);
}
return result;
}
module.exports = { objectMap };
Two changes fix both bugs. Writing into a fresh result instead of obj means the input survives untouched. And iterating Object.keys(obj) — which returns only own enumerable keys — instead of for...in means inherited prototype keys never enter the result. The mapper is called with (value, key, obj) so a caller can use the key ((v, k) => k + v) or ignore it (v => v * 2) freely.
Trace objectMap({ a: 1, b: 2 }, (v, k) => k + v) end to end.
Object.keys({ a: 1, b: 2 }) is ['a', 'b']. result starts as {}.
result = {}
key 'a' → mapper(1, 'a', obj) = 'a' + 1 = 'a1'
→ result['a'] = 'a1'
→ result = { a: 'a1' }
key 'b' → mapper(2, 'b', obj) = 'b' + 2 = 'b2'
→ result['b'] = 'b2'
→ result = { a: 'a1', b: 'b2' }
return { a: 'a1', b: 'b2' }
Note the mapper used both arguments: k + v concatenates the key string with the value. Because 'a' + 1 coerces the number to a string, the values changed type — from number to string. The keys, meanwhile, came out in the same order Object.keys produced them, and the original obj is still { a: 1, b: 2 }.
obj[key] = ... overwrites the caller's data — after the call their original object is destroyed. Build a separate const result = {} and assign into that; never write back into obj.for...in for the keys. for...in walks inherited enumerable prototype keys too, so an object made via Object.create({ inherited: 1 }) would map inherited as though it were its own. Iterate Object.keys(obj) (own enumerable keys only) instead — or guard each key with Object.prototype.hasOwnProperty.call(obj, key).return (e.g. (v) => { v * 2 } with braces), every value becomes undefined. The mapper's return value is the new value — make sure there is one.(v, k) => ...) or the whole object. If you call mapper(obj[key]) without passing key and obj, those callers silently get undefined for the missing arguments.objectMap transforms values and keeps keys fixed. If you also need to rename keys, that's a different operation (mapKeys) — don't conflate the two.Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.
Array.prototype.map builds a new array by running every element through a callback. There is no built-in equivalent for objects, so implement one. objectMap(obj, mapper) returns a new object with the same keys as obj, but every value replaced by mapper(value, key, obj). The original object is left untouched, just like Array.prototype.map never mutates the array it walks.
// obj: Record<string, V> — the source object to read.
// mapper: (value: V, key: string, obj) => U
// — called once per OWN key; its return value becomes the new value.
// returns: Record<string, U> — a new object, same keys, mapped values.
function objectMap(obj, mapper): Record<string, U>;
// Double every value.
objectMap({ a: 1, b: 2 }, (v) => v * 2);
// → { a: 2, b: 4 }
// The mapper also receives the key, so values can be built from both.
objectMap({ a: 1, b: 2 }, (v, k) => k + v);
// → { a: 'a1', b: 'b2' }
obj; only the values change. Values may become any type.mapper(value, key, obj) — mirroring Array.prototype.map's (element, index, array). The third argument is the original object.obj exactly as it was.objectMap({}, fn) returns {}.You'll build a new object that keeps the same keys as the input but runs each value through a callback first — Array.prototype.map, except for an object's values.
Say you have a price list: { apple: 1, pear: 2 }. You want the same list with tax added to every price. You don't want to touch the original — you want a fresh object, same product names, new numbers. objectMap is that operation generalized: hand it an object and a mapper, and it gives back a new object with identical keys whose values are whatever mapper returns for each one. The mapper is called as mapper(value, key, obj), the same shape as the callback you already know from Array.prototype.map.
Picture each key/value pair walking through the mapper one at a time. The key rides along unchanged; only the value is transformed. Whatever the mapper returns gets parked under the same key in a brand-new object. When every pair has passed through, that new object is your answer. The input is only ever read, never written.
The obvious move is to loop the object's keys and overwrite each value in place:
function objectMap(obj, mapper) {
for (const key in obj) {
obj[key] = mapper(obj[key], key, obj); // mutates the caller's object!
}
return obj;
}
This has two real bugs. First, it mutates the input — obj[key] = ... writes straight back into the caller's object, so after the call their original data is gone. Second, for...in doesn't stop at the object's own keys — it walks inherited enumerable keys up the prototype chain too. If the object was made with Object.create({ inherited: 1 }), the loop visits inherited as well, mapping a key that was never the object's own.
function objectMap(obj, mapper) {
// Start a FRESH object so the caller's input is never touched.
const result = {};
// Object.keys returns ONLY the object's own enumerable keys — inherited
// prototype keys are skipped, which is exactly the spec we want.
for (const key of Object.keys(obj)) {
// Call the mapper with the same (value, key, obj) shape as Array.map,
// and park its return value under the SAME key in the new object.
result[key] = mapper(obj[key], key, obj);
}
return result;
}
module.exports = { objectMap };
Two changes fix both bugs. Writing into a fresh result instead of obj means the input survives untouched. And iterating Object.keys(obj) — which returns only own enumerable keys — instead of for...in means inherited prototype keys never enter the result. The mapper is called with (value, key, obj) so a caller can use the key ((v, k) => k + v) or ignore it (v => v * 2) freely.
Trace objectMap({ a: 1, b: 2 }, (v, k) => k + v) end to end.
Object.keys({ a: 1, b: 2 }) is ['a', 'b']. result starts as {}.
result = {}
key 'a' → mapper(1, 'a', obj) = 'a' + 1 = 'a1'
→ result['a'] = 'a1'
→ result = { a: 'a1' }
key 'b' → mapper(2, 'b', obj) = 'b' + 2 = 'b2'
→ result['b'] = 'b2'
→ result = { a: 'a1', b: 'b2' }
return { a: 'a1', b: 'b2' }
Note the mapper used both arguments: k + v concatenates the key string with the value. Because 'a' + 1 coerces the number to a string, the values changed type — from number to string. The keys, meanwhile, came out in the same order Object.keys produced them, and the original obj is still { a: 1, b: 2 }.
obj[key] = ... overwrites the caller's data — after the call their original object is destroyed. Build a separate const result = {} and assign into that; never write back into obj.for...in for the keys. for...in walks inherited enumerable prototype keys too, so an object made via Object.create({ inherited: 1 }) would map inherited as though it were its own. Iterate Object.keys(obj) (own enumerable keys only) instead — or guard each key with Object.prototype.hasOwnProperty.call(obj, key).return (e.g. (v) => { v * 2 } with braces), every value becomes undefined. The mapper's return value is the new value — make sure there is one.(v, k) => ...) or the whole object. If you call mapper(obj[key]) without passing key and obj, those callers silently get undefined for the missing arguments.objectMap transforms values and keeps keys fixed. If you also need to rename keys, that's a different operation (mapKeys) — don't conflate the two.Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.