If you tie the parameters together using a generic type parameter, it will compile:
const updateObjectProperty = <K extends keyof MyObject>(key: K, value: MyObject[K]) => {
object[key] = value
}
updateObjectProperty("key1", "") //ok
updateObjectProperty("key2", "") // err
updateObjectProperty("key2", 2) // ok
Playground Link
The reason your version does not work is that you could pass in uncorrelated key and value:
updateObjectProperty("key2", "") // key2 now has a string
Playground Link
Now our new version is not 100% safe either, you could call it with key union, but that is less common:
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
Playground Link
You could do the same thing that TS does, and type value as an intersection of possible property values, and this will catch this loophole too, but that would require an assertion in the implementation again:
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
Playground Link