react-phone-number-input: `react-hook-form` component typings seem to be incorrect

The default import from 'react-phone-number-input/react-hook-form' has type any.

I assume this is because the type

type PhoneInputWithCountrySelectType<InputComponentProps = DefaultInputComponentProps, FormValues> = React.ComponentClass<Props<InputComponentProps, FormValues>, State<Props<InputComponentProps, FormValues>>>

is incorrect.

First of all, the type parameter FormValues follows an optional type parameter, which is not allowed in TypeScript.

In addition to that, FormValues is a required parameter, but the component type uses PhoneInputWithCountrySelectType without any parameters.

declare const PhoneInputWithCountrySelect: PhoneInputWithCountrySelectType;

As a result, it seems that TS cannot parse that and assigns any as the type.

As a workaround, I have swapped the parameters and assigned {} to FormValues, as I have no idea what it’s supposed to be, and it works.

About this issue

  • Original URL
  • State: closed
  • Created 2 years ago
  • Comments: 27 (17 by maintainers)

Most upvoted comments

@catamphetamine Actually, I just found out that there’s a new TS feature that allows you to fix a generic function’s type. So you can just write typeof PhoneInput<CustomProps, CustomFormValues> now.

In that case, we don’t need the export. Just move the generic to the right and it should all work great.

@catamphetamine It can be, it’s just that giving a type for the user’s wrapper component is not something you need to do. In my code base, most components are typed as FC<Props, Something>. Obviously I wouldn’t want a library trying to make me use something else. Especially since in my case FC is a custom type as well, so you can’t use it like that.

So, I don’t need a PhoneInputComponentType for my wrapper component, I just need a PhoneInputType to represent your component in case I need it.

@finkrer Thx for the explanation. I wonder why the following wouldn’t work though?

export type PhoneInputComponentType = FC<Props, PhoneInputType<PropsOf<CustomInputComponent>>>

Because it’s a type for the user component that wraps your component, so you don’t really know Props — those are the user component’s props. And CustomInputComponent is, of course, a custom component, so you don’t know it either. That’s why you have to expose a generic type (PhoneInputType in this case).

In any case, it’s totally fine for the user to define their own component’s type. What you can do is to make it easy for them to get your component’s type, hence the need for the second type.

@finkrer has previously mentioned that in that case, typeof PhoneInputWithCountrySelect wouldn’t work. I wonder why wouldn’t it? It has the default values for the generics, so why wouldn’t it simply assume typeof PhoneInputWithCountrySelect< = DefaultInputComponentProps> in such case?

Because it has opposite variance here.

When generating a PhoneInputWithCountrySelect, we can assume the user doesn’t mind a PhoneInputWithCountrySelect<DefaultInputComponentProps>.

When accepting a PhoneInputWithCountrySelect’s type as an argument (to get the type of its props), we can’t assume all PhoneInputWithCountrySelects are PhoneInputWithCountrySelect<DefaultInputComponentProps>. They might have custom input props, hence the inferred type is PhoneInputWithCountrySelect<unknown>, and the resulting props give you stuff like Control<unknown>, which isn’t accepted by the actual PhoneInputWithCountrySelect component.

The workaround is essentially something like this:

import { Props as PhoneNumberInputProps } from 'react-phone-number-input/react-hook-form-input'

const PhoneNumberInput: FC<Props, (props: PhoneNumberInputProps<PropsOf<CustomInputComponent>>) => JSX.Element> = (
  { name, control, hasError, ...rest },
  ref
) => {
  ...
  return (
    <PhoneInput<PropsOf<CustomInputComponent>>
      ref={ref}
      inputComponent={CustomInputComponent}
      name={name}
      control={control}
      {...rest}
    />
  )
}

The main problem here is that you have to describe the type of the PhoneInput again, saying, yeah, it’s a (props: PhoneNumberInputProps<PropsOf<CustomInputComponent>>) => JSX.Element>, which is a handful.

A solution would be to have a second type which is exactly as it is in the current version:

type PhoneInputType<InputComponentProps = DefaultInputComponentProps, FormValues = DefaultFormValues> = (props: Props<InputComponentProps, FormValues>) => JSX.Element;

Now this can be used to construct the props type, like so:

const PhoneNumberInput: FC<Props, PhoneInputType<PropsOf<CustomInputComponent>>> =

Which is as good as it gets.

So basically you need both types, one for the actual component’s type, that’s the one I proposed earlier, and the other to represent the component’s type with fixed generic parameters, that’s the one that is there right now. The second one needs to be exported as well.

You can.

I’ve played around with this a little, and while the generics work, they are also less convenient in some cases. If you have a wrapping component that adds PhoneInput’s props to its own, right now you can just use typeof PhoneInput to extract the types automatically. With generics, the parameters evaluate to <unknown, any> in this case, which doesn’t accept anything. As a workaround, any wrapper has to also be generic and construct the props type manually.

I’m not sure what the use would be for generics anyway. Normally generic components are useful because TS infers the type from one prop and uses it to validate others, but here each parameter is only used once. So it’s probably not worth it in this case.

Thanks for the quick fix, eveything works now.

If I go into TypeScript expert mode, looking at the types, I can also say that this makes FormValues useless. The API is essentially the same as before that commit I mentioned. FormValues is only used for Control, which already provides a default of Record<string, any>, as you noted. So you are now drilling through the type hierarchy only to replace the default type with the exact same type.

The only difference is that you can in theory import the generic type manually and provide a different value. But the exported component’s type is non-generic and can’t be changed by the developer.

You can safely leave this as it is, I just already typed that when you released the fix, haha.