Home > OS >  How to pass a Generic type to a functional component props which also extends another Interface for
How to pass a Generic type to a functional component props which also extends another Interface for

Time:11-26

Explantion

I'm creating a dynamic input form using the FormProvider from react-hook-form.

const formMethods = useForm<TSongFormData>();
return (
    <FormProvider {...formMethods}>
       <SongInputForm />
    </FormProvider>
)

In the SongInputForm component I have my actual form which will have the input components which I have created for every input elements like input teaxtarea and so on...

const SongForm = () => {
  const { handleSubmit } = useFormContext<TSongFormData>();
  return (
        <form
          onSubmit={handleSubmit((data) => console.log("data", data))}
        >
          <InputElement label="Song Name" name="name" />
          <InputElement label="Author" name="author" />
          <input type="submit" />
        </form>
    );
  };
}

The InputElementccomponent would receive all the props for the input element itself and the name prop would be a union type TSongFormDataKey of all the keys of the form data.

interface ITextInputProps extends React.InputHTMLAttributes<HTMLInputElement> {
  label?: string;
  name: TSongFormDataKey;
}

const InputElement = ({ name, label, ...restProps }: ITextInputProps) => {
  const { register } = useFormContext<TSongFormData>();
  return (
    <div>
      <label htmlFor={name}>{label}</label>
      <input id={name} {...restProps} {...register(name)} />
    </div>
  );
};

Question

As of now, I have hardcoded the types TSongFormData and TSongFormDataKey in the InputElement component. But how do I pass both the types TSongFormData and TSongFormDataKey as generics to the InputElement component so that I can make it dynamic and have more flexibility on the type of props passed to it??

What I'm looking for is something like this:

interface ITextInputProps<T> extends React.InputHTMLAttributes<HTMLInputElement> {
  label?: string;
  name: T;
}

const InputElement = <T,K>({ name, label, ...restProps }: ITextInputProps<T>) => {
  const { register } = useFormContext<K>();
  return (
    <div>
      <label htmlFor={name}>{label}</label>
      <input id={name} {...restProps} {...register(name)} />
    </div>
  );
};

where T would be TSongFormData and K would be TSongFormDataKey.

I've created a codesandbox if anyone wants to play with it: https://codesandbox.io/s/elastic-sanne-uz3ei8

I'm new to typescript and trying to get my head around generics, but finding it so hard.

Any help would be greatly appreciated. Thanks

CodePudding user response:

You can make the ITextInputProps type generic and the InputElement name generic. For name we can use the Path generic type from useFormContext. keyof T would also work, except register expects a Path<T>, and while that will include keyof T, while Path still has unresolved type parameters (such as T) typescript won't be able to follow that relationship.

When you instantiate the component, you will have to specify the argument explicitly: <InputElement<TSongFormData> label="Song Name" name="name" />

You can also create a specialized version of input field for a specific type using an instantiation expression in newer versions of TS:

const SongInputElement = InputElement<TSongFormData>

Putting it all together we get:


interface ITextInputProps<T> extends React.InputHTMLAttributes<HTMLInputElement> {
  label?: string;
  name: Path<T>;
}

const InputElement = <T extends FieldValues>({ name, label, ...restProps }: ITextInputProps<T>) => {
  const { register } = useFormContext<T>();
  return (
    <div>
      <label htmlFor={name}>{label}</label>
      <input id={name} {...restProps} {...register(name)} />
    </div>
  );
};



const SongForm = () => {
  const { watch, handleSubmit } = useFormContext<TSongFormData>();

  return (
    <InputElement<TSongFormData> label="Author" name="author" />
  );
};

const SongFormV2 = () => {  
  const { watch, handleSubmit } = useFormContext<TSongFormData>();
  const SongInputElement = InputElement<TSongFormData>
  return (
    <SongInputElement label="Song Name" name="name" />
  )
};

Playground Link

  • Related