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! 😊
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"'