I'm porting a project to typescript and everything went fine except for this simple utility function:
function mapObject(obj, mapperFn) {
return Object.fromEntries(
Object.entries(obj).map(([key, value]) => mapperFn(key, value))
);
}
It is used for transforming keys and values of an object, like so:
let obj = { a: 1, b: 2 };
let mappedObj = mapObject(obj, (key, value) => [
key.toUpperCase(),
value + 1,
]);
mappedObj; // { A: 2, B: 3 }
edit For the sake of clarity, the output object's entry values aren't necessarily identical to the ones in input object, e.g. the above example could've transformed the numbers into a string to result in { A: '2', B: '3' }, or just return null for each of them: { A: null, B: null }. Also, I only consider string keys in both input and output objects for simplicity and because that's how it is used in our project.
I'd like mapperFn to retain the object entries' value types, supporting type narrowing by key, just the same as GetEntryOf does in the example below:
type GetEntryOf<T> = {
[K in keyof T]: [K, T[K]];
}[keyof T];
interface TestI {
str: string;
num: number;
}
let entry: GetEntryOf<TestI>;
if (entry[0] === 'str') {
entry[1]; // string
} else if (entry[0] === 'num') {
entry[1]; // number
} else {
entry; // never
}
This is the closest I've got, unfortunately I needed to transition to using key in obj operator:
function mapObject<K>(
obj: K,
mapperFn: (key: keyof K, value: K[typeof key]) => [string, unknown]
): Record<string, unknown> {
const newObj = {} as Record<string, unknown>;
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
const value = obj[key];
const [newKey, newValue] = mapperFn(key, value);
newObj[newKey] = newValue;
}
}
return newObj;
}
interface TestI {
str: string;
num: number;
}
let test: TestI;
mapObject(test, (key, value) => {
if (key === 'str') {
return [key, value]; // value should be string
} else if (key === 'num') {
return [key, value]; // value should be number
} else {
return [key, value]; // value should be never
}
});
As you can see, value's type does not get narrowed correctly:
Considering that GetEntryOf can do it, I'd think that it should be possible to do the same inside mapperFn. Is this even possible in current typescript, and if so, what is the appropriate way to do it?
Notes: The working type narrowing example is based on this SO answer. I'm aware of and okay with the constraint mentioned in the same answer: "it is not guaranteed that the value does not also have other properties".
