The handleOnChange
function does not autocomplete the key
parameter and does not provide the correct type for value
parameter.
This does not work as intended.
export function MyComponent<T extends MyItem>(props: PropsWithChildren<MyComponentProps>) {
const { item, setItem } = props
function handleOnChange<K extends keyof T>(key: K) {
return function(value: T[K]) {
setItem({ ...item, [key]: value })
}
}
return <input value={item.name} onChange={e => handleOnChange('name')(e.target.value)} />
}
This does work as intended.
export function MyComponent<T extends MyItem>(props: PropsWithChildren<MyComponentProps>) {
const { item, setItem } = props
function handleOnChange<K extends keyof MyItem>(key: K) {
return function(value: MyItem[K]) {
setItem({ ...item, [key]: value })
}
}
return <input value={item.name} onChange={e => handleOnChange('name')(e.target.value)} />
}
Do you know why a generic would cause issues in this instance?
For the IntelliSense part of the question, it looks like the completion list just doesn't include keyof A
when examining keyof B
when B extends A
. It would be nice if it did, but it doesn't. Such a change has been suggested at microsoft/TypeScript#28662. If you want to see this happen you might want to go to that Github issue and give it your 👍 or describe your use case if you think it is both compelling and different from what's already there.
For now the workaround could be to change the constraint to be K extends keyof T | keyof MyItem
. Since keyof T
already must include keyof MyItem
, the additional union of keyof MyItem
doesn't change anything in terms of what types are valid specifications for K
. But it does bring back the IntelliSense autocompletion list you expect.
For the "correct type for value
parameter" part of the question, I think I need more elaboration on what you mean. But do be warned: if T extends MyItem
, then T["name"]
might be a narrower type than MyItem["name"]
. Imagine:
interface MyItem {
name: string;
}
export function MyComponent<T extends MyItem>() {
function handleOnChange<K extends keyof T | keyof MyItem>(key: K) {
return function (value: T[K]) { }
}
handleOnChange("name")("Fred"); // wait a minute
}
TypeScript allows you to call handleOnChange("name")(xxx)
with any string
as xxx
, even though this is not safe. Observe:
interface MyItemNamedBob extends MyItem {
name: "Bob";
}
MyComponent<MyItemNamedBob>(); // ?
A value of type MyItemNamedBob
has the literal string "Bob"
as the name
at the type level. It cannot be changed to "Fred"
. handleOnChange("name")
for a generic T
that extends MyItem
can't really safely accept anything at all. In practice this might not matter, but you should be careful, since the compiler isn't always.