Home > front end >  How to derive types from the passed arguments?
How to derive types from the passed arguments?

Time:01-04

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
}

Playground Link

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.

  • Related