Reputation: 103
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! 😊
Upvotes: 1
Views: 76
Reputation: 63142
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"'
Upvotes: 1
Reputation: 848
You can follow the below approach:
// types.tsx
export interface FormFields {
email: string;
name: string;
}
export interface InputProps {
label: string;
name: keyof FormFields; // The name must be one of the keys of FormFields (i.e., 'email' or 'name')
type?: string;
placeholder?: string;
}
// your-component.tsx
import { InputProps } from "./types"
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>
);
}
// your-form-component.tsx
import { FormFields } from "./types"
const methods = useForm<FormFields>();
Upvotes: 0