Can someone help me understand why the "Fails" line fails? And how I can make it work as I would expect, without the workaround casting?
function useObjectWithSingleNamedKey<Key extends string>(
keyName: Key,
value: string,
fn: (obj: {[key in Key]: string}) => void)
{
fn({ [keyName]: value }) // <-- Fails, but why?
fn({ [keyName]: value } as { [key in Key]: string }) // <-- Unwanted workaround
}
useObjectWithSingleNamedKey('foobar', 'test', obj => {
console.log(obj.foobar) // <-- Should work and does
console.log(obj.notHere) // <-- Should fail and does
});
I'm getting this error:
Argument of type '{ [x: string]: string; }' is not assignable
to parameter of type '{ [key in Key]: string; }'.(2345)
I want to understand why this currently fails, and how to make it not fail.
How can I type this function so that it type-wise still works "externally" as it does now (i.e. 'foobar' passed in as keyName is available on obj.foober, while obj.notHere or anything else is not), but I'm allowed to call the fn function without using an as cast.
Note: The javascript code works fine, it's the Typescript type check that fails and that I don't understand why it does.
The reason why it doesn't work is that it is technically possible for the generic
<Key>to be broader than just the literal string which is passed askeyName.obj: {[key in Key]: string}means thatobjmust have a value for every possible key assignable toKey. You are assuming that this is only one key, but typescript doesn't know that for certain.Here as an example to illustrate where the function fails. This function call is perfectly valid and has no typescript errors. The failures are inside the body of the function.
If we say that
Keyis the union of'foo' | 'bar', then the type of our objectobjhere is presumed to be{foo: string; bar: string;}. When the function creates an object from the key and value arguments, that object of type{foo: string}won't be sufficient for the expected callback.Currently there is not way to say that
Keycan only be a single literal string.You can use your existing workaround and assume that you won't pass any nonsensical arguments like my example.
Or you can rethink the function. It seems like you have a callback which acts upon objects with a specific key, and you want to be able to call that callback for a particular value directly. Do you need to pass key and value separately? Can you call your callback directly with ({ [keyName]: value })?
You can map specific callbacks to accept a value argument easily:
But we cannot generalize this mapping because we cannot be sure that the object has only one property.