Búsqueda de sitios web

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.

Artículos relacionados