Ad

Generics That Extend A Static Type Aren't Intellisensing

- 1 answer

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?

Ad

Answer

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. 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.


Okay, hope that helps; good luck!

Link to code

Ad
source: stackoverflow.com
Ad