Issue
I'm in ES7/ES8, I want to know how to map an object's values, regardless of its keys (like Array.map).
I found this post here
But :
- I am forced to map also the whole object, not the just value
- I also want it to give a strongly typed type inference when assigning to a variable.
- (If possible in a functional coding style)
// === TO MODIFY ===
type AnyObjectKeys = string | number;
type AnyObject<
Values,
Keys extends AnyObjectKeys = AnyObjectKeys,
> = Record<Keys, Values>;
type MapperFunc<T> = (
value: [AnyObjectKeys, T],
index: number,
array: [AnyObjectKeys, T][],
) => unknown
/**
* map an object's values, like Array<T>.map
* but can also access its key with another signature
*/
const objMapper =
<AnyObjectValues>(object: AnyObject<AnyObjectValues>) =>
(func: MapperFunc<AnyObjectValues>) =>
Object.assign({}, ...Object.entries(object).map(func));
// === TO MODIFY ===
const getAverage = (arr: number[]) =>
arr.reduce((a, b) => a + b, 0) / arr.length
// BASE OBJECT
const curvesPointsByDynamicKeys = {
['a']: {
x: [3, 4, 1],
y: [6, 1, 4],
z: [3, 9, 0],
},
['b']: {
x: [2, 1, 0],
y: [9, 3, 3],
z: [4, 6, 0],
},
// ...
};
// EXPECTED USAGE 1 (with key access)
const curvesAveragePointsByDynamicKeys = objMapper(curvesPointsByDynamicKeys)
(
([key, { x, y, z }]) => {
return {
id: `curve-${key}-${x.length}-${y.length}-${z.length}`,
averageX: getAverage(x),
averageY: getAverage(y),
averageZ: getAverage(z),
}
}
);
// EXPECTED USAGE 2 (without key access)
const curvesAveragePointsByDynamicKeys2 = objMapper(curvesPointsByDynamicKeys)
(
({ x, y, z }) => {
return {
averageX: getAverage(x),
averageY: getAverage(y),
averageZ: getAverage(z),
}
}
);
// EXPECTED RESULT 1 :
// {
// ['a']: {
// id: '...',
// averageX: 2.6666666666666665,
// averageY: 3.6666666666666665,
// averageZ: 4,
// },
// ['b']: {
// id: '...',
// averageX: 1,
// averageY: 5,
// averageZ: 3.3333333333333335,
// },
// // ...
// }
console.log(curvesAveragePointsByDynamicKeys)
// EXPECTED RESULT 2 :
// {
// ['a']: {
// averageX: 2.6666666666666665,
// averageY: 3.6666666666666665,
// averageZ: 4,
// },
// ['b']: {
// averageX: 1,
// averageY: 5,
// averageZ: 3.3333333333333335,
// },
// // ...
// }
console.log(curvesAveragePointsByDynamicKeys2)
// NOT EXPECTED RESULT :
// {
// averageX: 1,
// averageY: 5,
// averageZ: 3.3333333333333335,
// }
Solution
Your original version where the callback passed to the result of objMapper()
either accepts a single parameter of a key-value tuple type, or a single parameter of just the value type, is not going to work. At runtime, it's essentially impossible to inspect a function and understand what type of parameter it accepts. Such information doesn't really exist at runtime. So the implementation of objMapper()
would never know whether to pass [key, value]
or just value
to the callback.
Instead, we can write objMapper()
so that it always passes both value
and key
as two separate arguments to the callback. If the callback cares about both value
and key
, it should use them, like (value, key) => {}
. Otherwise, if it only wants value
then it should only look at the first argument, like (value) => {}
. This is acceptable, since functions with fewer parameters are assignable to functions that take more parameters.
Here's what that looks like:
const objMapper =
<K extends PropertyKey, VI>(obj: { [P in K]: VI }) =>
<VO,>(func: (v: VI, k: K) => VO) =>
Object.fromEntries(
Object.entries(obj).map(
([k, v]) => [k, func(v as any, k as any)])
) as { [P in K]: VO };
The call signature is essentially that you pass objMapper
an obj
of a type whose keys are of the generic type K
, and whose values are of the generic type VI
(the "input value" type). This returns another generic function which takes a callback func
of type (v: VI, k: K) => VO
, meaning it accepts both the input value v
and the key k
, and returns a value of the generic type VO
, the "output value" type. And the return type of this function is something whose keys are of generic type K
, and whose values are of the generic type VO
.
The implementation is using the Object.entries()
method to turn obj
into an array of entries, the map()
array method to transform the value with the func
callback, and the Object.fromEntries()
method to turn the entry array back into an object. (The compiler can't verify that this actually satisfies the call signature, so I used a bunch of type assertions to suppress errors. I presume you care more about the compiler verifying type safety for callers of objMapper
than you do for the implementer.)
Let's test it out on your example code, noting that the callback parameters either look like ({x, y, z}, key) => {}
or like ({x, y, z}) => {}
:
const curvesAveragePointsByDynamicKeys = objMapper(curvesPointsByDynamicKeys)(
({ x, y, z }, key) => {
return {
id: `curve-${key}-${x.length}-${y.length}-${z.length}`,
averageX: getAverage(x),
averageY: getAverage(y),
averageZ: getAverage(z),
}
}
);
console.log(curvesAveragePointsByDynamicKeys);
/* {
"a": {
"id": "curve-a-3-3-3",
"averageX": 2.6666666666666665,
"averageY": 3.6666666666666665,
"averageZ": 4
},
"b": {
"id": "curve-b-3-3-3",
"averageX": 1,
"averageY": 5,
"averageZ": 3.3333333333333335
}
} */
and
const curvesAveragePointsByDynamicKeys2 = objMapper(curvesPointsByDynamicKeys)
(
({ x, y, z }) => {
return {
averageX: getAverage(x),
averageY: getAverage(y),
averageZ: getAverage(z),
}
}
);
console.log(curvesAveragePointsByDynamicKeys2)
/* {
"a": {
"averageX": 2.6666666666666665,
"averageY": 3.6666666666666665,
"averageZ": 4
},
"b": {
"averageX": 1,
"averageY": 5,
"averageZ": 3.3333333333333335
}
}
*/
Looks good.
Answered By - jcalz
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.