I am trying to make a HOC which returns a component with forwardRef but am not sure how to type it Here is the code
type Omitted = 'variant' | 'size';
export interface InputProps<T extends Omit<T, Omitted>> {
startIcon?: React.ReactElement;
endIcon?: React.ReactElement;
}
const withInput = <P extends object>(InputComponent: React.ComponentType<P>) =>
React.forwardRef<HTMLInputElement, InputProps<P>>(
({ startIcon, endIcon, ...props }, ref) => {
return (
<InputGroup>
{startIcon && (
<InputLeftElement>
{startIcon}
</InputLeftElement>
)}
<InputComponent ref={ref} {...props} />
{endIcon && (
<InputRightElement>
{endIcon}
</InputRightElement>
)}
</InputGroup>
);
}
);
const Input = withInput(InputBaseComponent);
Input.Number = withInput(NumberInputBaseComponent);
But am getting two errors one on the InputComponent
Type '{ children?: ReactNode; ref: ((instance: HTMLInputElement | null) => void) | MutableRefObject<HTMLInputElement | null> | null; }' is not assignable to type 'P'.
'{ children?: ReactNode; ref: ((instance: HTMLInputElement | null) => void) | MutableRefObject<HTMLInputElement | null> | null; }' is assignable to the constraint of type 'P', but 'P' could be instantiated with a different subtype of constraint 'object'
and the other is on Input.Number
Property 'Number' does not exist on type 'ForwardRefExoticComponent<InputProps<Pick<InputProps, "variant" | "size" | "left" | "right" | "form" | "p" | "slot" | "style" | "title" | "pattern" | "ref" | "key" | "sx" | "accept" | "alt" | "autoComplete" | ... 514 more ... | "isLoading"> & Pick<...>> & RefAttributes<...>>'.
Here is a link to a codesandbox if someone wants to try it out: https://codesandbox.io/s/dazzling-shape-rwmmg?file=/src/Input.tsx:0-959
The Input.Number
error is the easier one to understand. You are trying to export an object which is a component and has a property Number
which is a different component. The error is telling you that you can't set an arbitrary property like Number
on a component, which kinda makes sense (It is doable, but complicated). I recommend putting both the Base
and the Number
at the same level rather than having one as a property of the other.
const Input = {
Base: withInput(InputBaseComponent),
Number: withInput(NumberInputBaseComponent)
}
Now Input
is just an object with two different components as properties.
You could also export the various instances individually, and group them together when you import by doing
import * as Input from `./Input`
First of all, I think that your InputProps
interface isn't doing what you intended for it to do.
export interface InputProps<T extends Omit<T, Omitted>> {
startIcon?: React.ReactElement;
endIcon?: React.ReactElement;
}
This interface says that InputProps
is an object with an optional startIcon
and endIcon
. That's it. T
is never used and doesn't matter. No props of T
are included in InputProps
. When you spread InputProps
into { startIcon, endIcon, ...props }
, the only prop that's left in ...props
is children
. So of course this is going to cause an error when we want ...props
to include all of the props P
of InputComponent
.
I think what you meant to say here was that InputProps<T>
contains those two icons AND all of T
, except for the omitted properties.
export type InputProps<T> = Omit<T, Omitted> & {
startIcon?: React.ReactElement;
endIcon?: React.ReactElement;
}
This is better because now we've got some actual props in ...props
. Hovering over it shows
Pick<React.PropsWithChildren<InputProps<P>>, "children" | Exclude<Exclude<keyof P, Omitted>, "startIcon" | "endIcon">>
So basically we've got everything in P
plus children
minus variant
, size
, startIcon
, and endIcon
. Is this subset assignable to P
? Maybe, but not always, and that's what the error is telling you.
It's common for typescript to have a hard time understanding that the pieces make up the whole when using {...rest}
syntax. I often have to assert as P
in my HOCs.
Regardless of everything in the next few paragraphs, you're still going to end up doing that here:
<InputComponent {...props as P} ref={ref} />
Is is really ok to say that ...props
is P
? When we use as
, we are basically telling typescript to hush because we know more than it does. So we want to make sure that we aren't giving it bad information.
When will {...props} not be assignable to P
? How can we prevent those instances from occurring?
There is some cause for concern here due to the fact that we Omit
variant
and size
from the props of the inner component, so ...props
(which we want to be P
) is known by typescript to not contain either of these properties. If these properties exist on type P
and are required, then ...props
cannot be P
. The same is true if P
includes required properties startIcon
and endIcon
, since we pulled those out with the spread.
For variant
and size
, I don't really see the point to omitting them at all. You aren't setting any default values for them, so why not let them be passed through?
But as a general idea, and for startIcon
and endIcon
, we need to refine our P extends
such that if P
has these properties, they must be optional. We also want to make sure that our InputComponent
can accept the ref
type which we are passing it.
You can be more confident that what we are asserting with as
is accurate if you refine the types like this:
interface Dropped {
variant?: any;
size?: any;
}
interface Icons {
startIcon?: React.ReactElement;
endIcon?: React.ReactElement;
}
export type InputProps<T> = Omit<T, keyof Dropped | "ref"> & Icons;
type PBase = {
ref?: React.Ref<HTMLInputElement>;
} & Dropped & Partial<Record<keyof Icons, any>>
const withInput = <P extends PBase>(InputComponent: React.ComponentType<P>) => ....
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.