Crear una función genérica de TypeScript que asigne un valor a una propiedad de un objeto
Quiero crear una función simple que tome la clave de un objeto específico y el valor para la clave respectiva y asigne el nuevo valor al objeto. Algo así:
interface MyObject {
key1: string
key2: number
}
const object: MyObject = {
key1: 'abc',
key2: 100
}
const updateObjectProperty = (key: keyof MyObject, value: MyObject[keyof MyObject]) => {
object[key] = value
}
Pero en este caso, no funciona. El compilador de TypeScript muestra un error:
Type 'string | number' is not assignable to type 'never'.
Siento que tal vez algunos genéricos podrían potencialmente resolver el problema aquí, pero me cuesta averiguar cómo.
Respuesta #1
Si unes los parámetros usando un parámetro de tipo genérico, se compilará:
const updateObjectProperty = <K extends keyof MyObject>(key: K, value: MyObject[K]) => {
object[key] = value
}
updateObjectProperty("key1", "") //ok
updateObjectProperty("key2", "") // err
updateObjectProperty("key2", 2) // ok
Enlace del parque
La razón por la que tu versión no funciona es que podrías pasar una clave y un valor no correlacionados:
updateObjectProperty("key2", "") // key2 now has a string
Enlace del parque
Ahora nuestra nueva versión tampoco es 100% segura, podrías llamarla con key union, pero eso es menos común:
let k: keyof MyObject = Math.random() > 0.5 ? "key1" : "key2"
let v: MyObject[keyof MyObject] = Math.random() > 0.5 ? 0 : "key2"
updateObjectProperty(k, v); // fails when key and value are mssmatched
Enlace del parque
Podrías hacer lo mismo que hace TS, y tipar value como una intersección de posibles valores de propiedad, y esto también atraparía esta laguna, pero eso requeriría nuevamente una afirmación en la implementación:
type ValueIntersectionByKeyUnion<T, TKey extends keyof T> = {
[P in TKey]: (k: T[P])=>void
} [TKey] extends ((k: infer I)=>void) ? I : never
const updateObjectProperty = <K extends keyof MyObject>(key: K, value: ValueIntersectionByKeyUnion<MyObject, K>) => {
object[key] = value as MyObject[K];
}
let k: keyof MyObject = Math.random() > 0.5 ? "key1" : "key2"
let v: MyObject[keyof MyObject] = Math.random() > 0.5 ? 0 : "key2"
updateObjectProperty(k, v); //err
updateObjectProperty("key1", "") //ok
updateObjectProperty("key2", "") // err
updateObjectProperty("key2", 2) // ok
Enlace del parque
Respuesta #2
El problema es que no estás especificando una relación entre key y value. Estás diciendo que key puede ser cualquier propiedad en MyObj y que value puede ser del tipo de cualquiera de las propiedades de ese objeto.
Si defines un genérico T como keyof MyObject y lo utilizas para definir ambos parámetros, obtendrás el comportamiento que estás buscando.
Ver un ejemplo en vivo
function updateObjectProperty<T extends keyof MyObject>(key: T, value: MyObject[T]) {
object[key] = value;
}
Observa las ondulaciones que te muestran los parámetros inválidos pasados.
