Skapa en generell TypeScript-funktion som tilldelar ett värde till ett objektets egenskap


Jag vill skapa en enkel funktion som tar nyckel för ett specifikt objekt och värde för respektive nyckel och tilldelar det nya värdet till objektet. Något liknande detta:

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
}

Men i det här fallet fungerar det inte. TypeScript-kompilatorn visar ett fel:

Type 'string | number' is not assignable to type 'never'.

Jag känner att kanske några generiska lösningar potentiellt skulle kunna lösa problemet här, men jag har svårt att lista ut hur.

Svar #1

Om du binder parametrarna tillsammans med hjälp av en generisk typparameter kommer det att kompileras:

const updateObjectProperty = <K extends  keyof MyObject>(key: K, value: MyObject[K]) => {
    object[key] = value
}

updateObjectProperty("key1", "") //ok
updateObjectProperty("key2", "") // err
updateObjectProperty("key2", 2) // ok

Lekplatslänk

Anledningen till att din version inte fungerar är att du kan skicka in okorrelerade nyckel och värde:

updateObjectProperty("key2", "") // key2 now has a string

Lekplatslänk

Nu är vår nya version inte 100% säker heller, du skulle kunna kalla den med key union, men det är mindre vanligt:

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

Lekplatslänk

Du skulle kunna göra samma sak som TS gör och skriva värdet som en korsning av möjliga egenskapsvärden, och detta kommer också att fånga detta kryphål, men det skulle kräva en antydan i implementeringen igen:

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

Lekplatslänk

Svar #2

Problemet är att du inte specificerar ett förhållande mellan key och value. Du säger att key kan vara vilken egenskap som helst i MyObj och att value kan vara typen av vilken som helst av objektets egenskaper.

Om du definierar en generisk T som keyof MyObject och använder den för att definiera båda parametrarna, kommer du att få det beteende du letar efter.

Se ett levande exempel

function updateObjectProperty<T extends keyof MyObject>(key: T, value: MyObject[T]) {
    object[key] = value;
}

Observera de kråkstreck som visar att ogiltiga parametrar har skickats.

Relaterade artiklar