TypeScript: Cómo manejar los tipos genéricos y el operador keyof
Intento escribir una función genérica que ensamblaje los datos de actualización para las actualizaciones de la base de datos.
Argumentos pasados:
- registro a actualizar
- clave de propiedad
- un nuevo elemento de la matriz
Aunque restrinjo el tipo de la clave usando keyof R, no puedo asignar un nuevo objeto con esa clave a una constante de tipo Partial<R>. Recibo el error Type '{ [x: string]: any[]; }' is not assignable to type 'Partial<R>'. ¿Qué puedo hacer para que el siguiente código funcione? Si reemplazo el tipo genérico R por un tipo no genérico, funciona. Pero esto no es lo que necesito.
Fragmento en TypeScript Playground
interface BaseRecord {
readonly a: ReadonlyArray<string>
}
function getUpdateData<R extends BaseRecord>(record: R, key: keyof R, newItem: string) {
const updateData: Partial<R> = { [key]: [...record[key], newItem] }
return updateData
}
interface DerivedRecord extends BaseRecord {
readonly b: ReadonlyArray<string>
readonly c: ReadonlyArray<string>
}
const record: DerivedRecord = { a: [], b: [], c: ["first item in c"] }
console.log(getUpdateData<DerivedRecord>(record, "c", "second item in c"))
Respuesta #1
Siempre puedes doblar el sistema de tipos a tu voluntad, ya sea mediante la astucia (por ejemplo, el acceso por índice y el compilador asumiendo que R[key] es de lectura-escritura)
function getUpdateData<R extends BaseRecord>(record: R, key: keyof R, newItem: string) {
var updateData: Partial<R> = {};
updateData[key] = [...record[key], newItem];
return updateData
}
o fuerza bruta (pasar a través de any tipo):
function getUpdateData<R extends BaseRecord>(record: R, key: keyof R, newItem: string) {
const updateData: Partial<R> = <any> { [key]: [...record[key], newItem] }
return updateData
}
Lo anterior responde a tu pregunta, pero ten cuidado: esta función no es segura. Asume que cualquier record que se pase tendrá un valor string[] para la propiedad key, pero el tipo R podría no serlo. Por ejemplo:
interface EvilRecord extends BaseRecord {
e: number;
}
var evil: EvilRecord = { a: ['hey', 'you'], e: 42 };
getUpdateData(evil, 'e', 'kaboom'); // compiles okay but runtime error
Además, el tipo de valor de retorno Partial<R> es un poco demasiado amplio: sabes que tendrá la clave key, pero tendrás que verificarla para que el sistema de tipos esté satisfecho:
var updatedData = getUpdateData<DerivedRecord>(record, "c", "first item in c") // Partial<DerivedRecord>
updatedData.c[0] // warning, object is possibly undefined
Sugeriría escribir getUpdateData() de esta manera:
type KeyedRecord<K extends string> = {
readonly [P in K]: ReadonlyArray<string>
};
function getUpdateData<K extends string, R extends KeyedRecord<K>=KeyedRecord<K>>(record: R, key: K, newItem: string) {
return <KeyedRecord<K>> <any> {[key as string]: [...record[key], newItem]};
}
(nota: esto todavía es difícil de escribir correctamente debido a un error en TypeScript) Ahora la función solo aceptará algo donde la propiedad key sea del tipo ReadonlyArray<string>, y garantiza que la propiedad key esté presente en el valor devuelto:
var evil: EvilRecord = { a: ['hey', 'you'], e: 42 };
getUpdateData(evil, 'e', 'kaboom'); // error, number is not a string array
var updatedData = getUpdateData(record, "c", "first item in c") // KeyedRecord<"c">
updatedData.c[0] // no error
Espero que eso ayude.
Actualización técnica
Cambié la declaración sugerida getUpdateData() arriba para que tenga dos parámetros genéricos, porque por alguna razón TypeScript estaba infiriendo un tipo demasiado amplio para el parámetro key antes, obligándote a especificar el tipo de clave en el sitio de llamada:
declare function oldGetUpdateData<K extends string>(record: KeyedRecord<K>, key: K, newItem: string): KeyedRecord<K>;
oldGetUpdateData(record, "c", "first item in c"); // K inferred as 'a'|'b'|'c', despite the value of 'c'
oldGetUpdateData<'c'>(record, "c", "first item in c"); // okay now
Al agregar un segundo genérico, aparentemente retrasé la inferencia del tipo de registro de TypeScript después de que infiere correctamente el tipo de clave:
getUpdateData(record, "c", "hello"); // K inferred as 'c' now
Siéntete libre de ignorar esto, pero así es como se hace la salchicha con inferencia de tipo heurística.
