Reputation: 63
I want to create a simple function that will take key of a specific object and value for the respective key and assign the new value to the object. Something like this:
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
}
But in this case - it's not working. TypeScript complier shows an error:
Type 'string | number' is not assignable to type 'never'.
I feel like maybe some generics could potentially resolve the problem here, but I'm struggling to figure out how.
Upvotes: 3
Views: 4961
Reputation: 3821
Can you try
const updateObjectProperty = <T extends keyof MyObject>(key: T, value: MyObject[T]) => {
object[key] = value
}
Upvotes: 0
Reputation: 92334
The problem is that you are not specifying a relationship between key
and value
. You are saying that key
can be any property in MyObj
and that value
can be the type of any of that object's properties.
If you define a generic T
as keyof MyObject
and use that to define both parameters, you will get the behavior you are looking for.
function updateObjectProperty<T extends keyof MyObject>(key: T, value: MyObject[T]) {
object[key] = value;
}
Notice the squigglies showing you invalid parameters passed.
Upvotes: 2
Reputation: 250366
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
The reason your version does not work is that you could pass in uncorrelated key and value:
updateObjectProperty("key2", "") // key2 now has a string
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
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
Upvotes: 3