Reputation: 133
I created a util function, mapObject
, which is Array.map
but for objects.
It's used like so...
mapObject(
(value) => value * 100,
{a: 1, b: 2, c: 3}
)
// {a: 100, b: 200, c: 300}
Here's the implementation. I got a pretty type-safe version working, but had to silence 2 errors about using obj[key]
without an index signature.
type ValueOf<T> = T[keyof T];
export const mapObject = <OldObject, NewValue>(
mappingFn: (value: ValueOf<OldObject>) => NewValue,
obj: OldObject
): Record<keyof OldObject, NewValue> =>
Object.keys(obj).reduce((newObj, key) => {
// @ts-ignore (Not sure how to work around this)
const oldValue = obj[key];
// @ts-ignore (Not sure how to work around this)
newObj[key] = mappingFn(oldValue);
return newObj;
}, {} as Record<keyof OldObject, NewValue>);
What's a better way to code this? The function correctly infers what's being passed in, and the return value has great type-safety. I'd love to remove the @ts-ignore
s, but without using index signatures as they hurt type-safety.
Upvotes: 1
Views: 3823
Reputation: 11581
Try ramda's mapObjIndexed.
export function mapObjIndexed<T, TResult, TKey extends string>(
fn: (value: T, key: TKey, obj?: Record<TKey, T>) => TResult,
obj: Record<TKey, T>
): Record<TKey, TResult>;
Upvotes: 0
Reputation: 13003
If you use a for loop instead of reduce
then TypeScript is able to correctly track the type of the i
iterator variable.
Otherwise, as Psidom pointed out, Object.keys()
returns string[]
which loses the property:value mapping.
Here's the minimally changed version of your code:
type ValueOf<T> = T[keyof T];
export const mapObject = <OldObject extends object, NewValue>(
mappingFn: (value: ValueOf<OldObject>) => NewValue,
obj: OldObject
): Record<keyof OldObject, NewValue> => {
let newObj = {} as Record<keyof OldObject, NewValue>;
for (let i in obj) {
if (obj.hasOwnProperty(i)) {
const oldValue = obj[i];
newObj[i] = mappingFn(oldValue);
}
}
return newObj;
}
But there are a few different ways of annotating it to achieve the same result, the semantics vary slightly.
Here's an alternative version using mapped types (it looks similar but it's not an index signature).
This might be slightly more flexible because the input object doesn't need to be a Record
type with values of the same type.
The implementation is the same, it's just the type annotations which are different:
type ValueOf<T> = T[keyof T];
type MapTo<T, U> = {
[P in keyof T]: U
}
function mapObject<T extends object, U>(mappingFn: (v: ValueOf<T>) => U, obj: T): MapTo<T, U> {
let newObj = {} as MapTo<T, U>;
for (let i in obj) {
if (obj.hasOwnProperty(i)) {
const oldValue = obj[i];
newObj[i] = mappingFn(oldValue);
}
}
return newObj;
}
Upvotes: 3
Reputation: 214957
Object.keys
return string[]
. One possible solution is to use type assertion on Object.keys
as suggested in this stack overflow answer:
(Object.keys(obj) as Array<keyof typeof obj>).reduce((newObj, key) => {
...
})
Upvotes: 1