简体   繁体   中英

Correctly typing a HOC component that returns a component with forwardRef

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

Property 'Number' does not exist

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`

Assigning ...props to 'P'

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} />

Type Safety

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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM