I'm trying write a well-typed equivalent of lodash's mapValues
which will have a return type with the same shape the original object, not just a Record<x,y>
.
Given a source object:
const orig = {
a: {
get: () => "foo",
},
b: {
get: () => 5,
},
c: {
get: () => true,
}
};
Then mapValues(x => x.get(), orig)
would return
{
a: "foo",
b: 5,
c: true,
}
And its type would be
{
a: string,
b: number,
c: boolean
}
My first effort is generic on the respective values:
export const mapValues = <V1, V2>(
f: (v: V1) => V2,
obj: {[k: string]: V1},
): {[k: string]: V2} => {
const result: {[k: string]: V2} = {};
for (const k in obj) {
result[k] = f(obj[k]);
}
return result;
};
But the type ends up being
{
[k: string]: string | number | boolean;
}
My best effort so far (which doesn't compile) is:
export const mapValues2 = <TInput, TOutput>(
f: <K extends keyof TInput>(v: TInput[K]) => (K extends keyof TOutput) ? TOutput[K] :never,
obj: TInput,
): TOutput => {
const result: Partial<TOutput> = {};
for (const k in obj) {
result[k] = f<typeof k>(obj[k]);
}
return result as TOutput;
};
Object.keys
doesn't correspond to the type of the object you're reflecting on. If you're doing this for your own use and understand that limitation (i.e. that it isn't actually type-safe) cool, but there's a reason libraries don't. Otherwise use type-safe bespoke access.get
) like thisas
:)