I have a component with a methods
parameter and its type definitions come from an imported library.
import { useForm } from "react-hook-form";
interface FormParams {
commonField: string
optionalField1: string
optionalField2: number
}
function App() {
const methods = useForm<FormParams >();
const { register, handleSubmit, watch } = methods
useCustomHook({ methods, optionalFieldName: 'optionalField2' })
}
In this component, I call a function which expects this methods
parameter and the name of one of the optional form parameters as arguments.
And here's the implementation for this function;
import { FieldPath, FieldValues, useForm, UseFormReturn } from "react-hook-form";
type CustomFieldValues = FieldValues & {
commonField: string
}
type HookParams = {
methods: UseFormReturn<CustomFieldValues>
optionalFieldName?: FieldPath<CustomFieldValues>
}
const useCustomHook = ({ methods, optionalFieldName = 'optionalField1' }: HookParams) => {
const { watch } = methods
const fieldValue = watch(optionalFieldName) // ❌ returns with type `any` because of wrong typing
const commonFieldValue = watch('commonField') // ✅ returns with correct `string` type
// do some stuff
}
The types for the function is not complete. In here watch
function returns the current value for the given form parameter name. Currently, commonFieldValue
is correctly typed as string
but fieldValue
typed as any
.
If I were to be able to use UseFormReturn<FormParams>
for methods
I wouldn't have this problem but this function is actually being called from a lot of components with different form parameters. Only parameter that is common is commonField
. And the other field is based on the argument passed to the function. This code may look useless but I tried to simplify it as much as possible to focus on the problem.
So if I call the function with;
useCustomHook({ methods, optionalFieldName: 'optionalField2' })
this fieldValue
parameter should be typed as number
.
const fieldValue = watch(optionalFieldName)
if I call the function with;
useCustomHook({ methods })
this fieldValue
parameter should be typed as string
(because default value is optionalField1
).
const fieldValue = watch(optionalFieldName)
Can we define a generic type for HookParams so I can get the correct type definition inside the function?
Here's the sandbox for the problem.
CodePudding user response:
You can't do this 100% type safe. The way UseFormReturn
it is invariant (so there is no inheritance relationship between UseFormReturn<CustomFieldValues>
and UseFormReturn<FormParams>
, see my talk on variance here)
You can however use to more relaxed checking of assignability when it comes to overloads. So we will have a public function signature that allows us to pass in the derived type and an implementation signature that will use UseFormReturn<CustomFieldValues>
. Now I want to stress this will not be 100% type safe, you might be able to do things that you should be allowed, but this is as good it's going to get
function useCustomHook<T extends CustomFieldValues>(args: HookParams<T>): void
function useCustomHook({ methods, optionalFieldName = 'optionalField1' }: HookParams<CustomFieldValues & Record<string, unknown>>) {
const { watch } = methods
const fieldValue = watch(optionalFieldName) // We don't know what type the field will have so we get unknown (from & Record<string, unknown>)
const commonFieldValue = watch('commonField') // ✅ returns with correct `string` type
}
Note: On the implementation signature I also had to add Record<string, unknown>
. This is to allow us to index with an arbitrary property that we don't know. This does mean you can call watch
with any string, but you will get unknown
, if you call watch with one on the known properties however, you will get the correct type.