typescript - Mapeando objeto con la reducción de tipo correcta en la función de mapeo
Estoy trasladando un proyecto a TypeScript y todo salió bien excepto por esta sencilla función utilitaria:
function mapObject(obj, mapperFn) {
return Object.fromEntries(
Object.entries(obj).map(([key, value]) => mapperFn(key, value))
);
}
Se utiliza para transformar las claves y los valores de un objeto, de la siguiente manera:
let obj = { a: 1, b: 2 };
let mappedObj = mapObject(obj, (key, value) => [
key.toUpperCase(),
value + 1,
]);
mappedObj; // { A: 2, B: 3 }
editar Para mayor claridad, los valores de las entradas del objeto de salida no son necesariamente idénticos a los del objeto de entrada, por ejemplo, el ejemplo anterior podría haber transformado los números en una cadena para resultar en { A: '2', B: '3' }, o simplemente devolver null para cada uno de ellos: { A: null, B: null }. Además, solo considero claves de tipo cadena tanto en los objetos de entrada como en los de salida por simplicidad y porque así es como se usa en nuestro proyecto.
Me gustaría que mapperFn mantuviera los tipos de valor de las entradas del objeto, compatibles con la restricción de tipo por clave, tal como lo hace GetEntryOf en el ejemplo a continuación:
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
}
Esto es lo más cercano que he logrado, desafortunadamente necesitaba pasar a usar el operador key in obj:
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
}
});
Como puedes ver, el tipo de value no se reduce correctamente:
Considerando que GetEntryOf puede hacerlo, pensaría que debería ser posible hacer lo mismo dentro de mapperFn. ¿Es esto siquiera posible en el TypeScript actual, y de ser así, cuál es la manera apropiada de hacerlo?
Notas: El ejemplo de restricción de tipo que funciona se basa en esta respuesta de SO. Soy consciente de la limitación mencionada en la misma respuesta y estoy de acuerdo con ella: "no se garantiza que el valor no tenga también otras propiedades".
Respuesta #1
Creo que estás buscando algo como esto:
type Values<T> = T[keyof T]
type MakeTuple<T> = Values<{
[Prop in keyof T]: { key: Prop, value: T[Prop] }
}>
type Output<T> = {
[Prop in keyof T]: [Prop, T[Prop]]
}
const mapObject = <K,>(
obj: K,
mapperFn: (params: MakeTuple<K>) => [MakeTuple<K>['key'], MakeTuple<K>['value']]
) => (Object.keys(obj) as Array<keyof K>)
.reduce((acc, key) => {
const [newKey, newValue] = mapperFn({ key, value: obj[key] });
return {
...acc,
[newKey]: newValue
}
}, {} as Output<K>)
interface TestI {
str: string;
num: number;
}
declare let test: TestI;
const result = mapObject(test, (obj) => {
// const key = params[0]
// const value = params[1]
if (obj.key === 'str') {
return [obj.key, obj.value]
} else if (obj.key === 'num') {
return [obj.key, obj.value];
} else {
throw new Error();
}
});
result.num // ['num', number]
Patio de recreo
No estoy seguro de por qué estás usando Object.prototype.hasOwnProperty.call(obj, key), pero puedes usar:
const hasProperty = <Obj, Prop extends string>(obj: Obj, prop: Prop)
: obj is Obj & Record<Prop, unknown> =>
Object.prototype.hasOwnProperty.call(obj, prop);
en su lugar. Incluso puedes sobrecargar hasOwnProperty como lo hice aquí
ACTUALIZACIÓN Se le permitirá usar dos argumentos separados en
const result = mapObject(test, (key, value) => {
if (key === 'str') {
return [key, obj]
} else if (key === 'num') {
return [key, obj];
} else {
throw new Error();
}
});
cuando se fusionará este PR de análisis de flujo de control para uniones discriminadas desestructuradas
Respuesta #2
Basado en la respuesta de captain-yossarian, este es el resultado final que se implementará:
type Values<T> = T[keyof T];
type MakeTuple<T> = Values<{
[Prop in keyof T]: { key: Prop; value: T[Prop] };
}>;
interface MapObjectMapper<K> {
(params: MakeTuple<K>): [string, unknown];
}
export default function mapObject<K>(
obj: K,
mapperFn: MapObjectMapper<K>
): Record<string, unknown> {
return Object.fromEntries(
(Object.keys(obj) as Array<keyof K>).map((key) =>
mapperFn({ key, value: obj[key] })
)
);
}
He extraído la firma de la función del mapeador por el bien de la legibilidad y he reemplazado el tipo de salida con [string, unknown] para tener en cuenta la nota de edición en la pregunta (usando solo claves de tipo string) y el hecho de que el valor de retorno de hecho no puede conocerse.
