reactjstypescriptnext.jsreact-hook-form

How to make name type-safe in a reusable input component using useFormContext from React Hook Form?


I'm using React Hook Form with useFormContext() in my Next.js project, and I’m trying to build a reusable input component. The problem I’m facing is that the name prop, which is used for registering the input field, is just a string.

I want TypeScript to suggest only valid field names based on my form schema instead of allowing any random string.

import { ExclamationCircleIcon } from "@heroicons/react/20/solid";
import React from "react";
import { useFormContext } from "react-hook-form";

interface InputProps {
  label: string;
  name: string; // ❌ I want this to be type-safe and suggest only valid form fields
  type?: string;
  placeholder?: string;
}

export function InputWithValidationError({ label, name, type = "text", placeholder }: InputProps) {
  const { register, formState: { errors } } = useFormContext(); // Accessing form context

  const error = errors[name]?.message as string | undefined;

  return (
    <div>
      <label htmlFor={name}>{label}</label>
      <input
        id={name}
        type={type}
        placeholder={placeholder}
        {...register(name)} // ❌ This allows any string, but I want it to be type-safe
      />
      {error && <p>{error}</p>}
    </div>
  );
}

Schema is defined like this:

const methods = useForm<{ email: string; password: string }>();

I use my component like this:

<InputWithValidationError label="Email" name="email" />

✅ TypeScript should suggest only "email" and "password"

❌ It should NOT allow random strings like "username" or "test"

My Question: How can I make the name prop type-safe so that TypeScript only allows valid form field names based on the form schema from useFormContext()?

Would really appreciate any help! 😊


Solution

  • You need to make the input component generic over the form fields, and then constrain name to be any path to a field.

    import { ExclamationCircleIcon } from "@heroicons/react/20/solid";
    import React from "react";
    import { useFormContext, FieldPath, FieldValues } from "react-hook-form";
    
    interface InputProps<TFieldValues extends FieldValues> {
      label: string;
      name: FieldPath<TFieldValues>;
      type?: string;
      placeholder?: string;
    }
    
    export function InputWithValidationError<TFieldValues extends FieldValues>({ label, name, type = "text", placeholder }: InputProps<TFieldValues>) {
      const { register, formState: { errors } } = useFormContext<TFieldValues>(); // Accessing form context
    
      const error = errors[name]?.message as string | undefined;
    
      return (
        <div>
          <label htmlFor={name}>{label}</label>
          <input
            id={name}
            type={type}
            placeholder={placeholder}
            {...register(name)} // This is constrained to valid names
          />
          {error && <p>{error}</p>}
        </div>
      );
    }
    

    You would then use it like:

    type FormValues = { email: string; password: string };
    
    const methods = useForm<FormValues>();
    
    <InputWithValidationError<FormValues> label="Email" name="email" />
    <InputWithValidationError<FormValues> label="Email" name="address" /> // ❌ Type '"address"' is not assignable to type '"email" | "password"'