Is it possible to create a strictly typed React HOC in Typescript?

Viewed 281

As context, I'm attempting to create a higher order component to wrap React Native components and add some functionality based on a ref to the component.

Short, untyped example:

export function withHelper(WrappedEl) {
  return ({
    layoutParam,
    onChangeLayout,
    ...wrappedElProps
  }) => {
    const ref = useRef(null);
    const onLayout = useCallback(() => {
      ref.current?.measure((_x, _y, width, height, px, py) => {
        onChangeLayout({
          layout: {
            width,
            height,
            y: py,
            x: px,
          },
          layoutParam,
        });
      });
    }, [
      onChangeLayout,
      layoutParam,
    ]);

    return <WrappedEl {...wrappedElProps} ref={ref} onLayout={onLayout} />;
  };
}

measure is a React Native element that gives me some information I need about the element for rendering other things. It's from the interface NativeMethods.

What I'd like to get strictly typed here:

  • The HOC can only be used on elements that have the measure method.
  • The resulting component is appropriately typed with all the original component's props as well as the two added props (layoutParam and onChangeLayout).

This is how far I got:

interface WithHelperProps {
  layoutParam: string;
  onChangeLayout: (props: {layout: Layout, payoutParam: string}) => void;
}

export function withHelper<
  P extends object,
  V extends React.ComponentClass<P> & Constructor<NativeMethods>,
>(El: V): React.FC<P & WithHelperProps> {
  return ({
    layoutParam,
    onChangeLayout,
    ...props
  }) => {
    const ref = useRef<InstanceType<V>>(null);
    const onLayout = useCallback(() => {
      ref.current?.measure((_x, _y, width, height, px, py) => {
        onChangeLayout({
          layout: {
            width,
            height,
            y: py,
            x: px,
          },
          layoutParam,
        });
      });
    }, [
      onChangeLayout,
      layoutParam,
    ]);

    const passthroughProps: Omit<
      PropsWithChildren<P & WithHelperProps>,
      keyof WithHelperProps
    > = props;

    return <El {...passthroughProps} ref={ref} onLayout={onLayout} />;
  };
}

But this doesn't compile:

Type 'Omit<PropsWithChildren<P & WithHelperProps>, keyof WithHelperProps> & { ref: RefObject<InstanceType<V>>; onLayout: () => void; }' is not assignable to type 'IntrinsicAttributes & IntrinsicClassAttributes<Component<P, any, any> & NativeMethods> & LibraryManagedAttributes<...>'.
  Type 'Omit<PropsWithChildren<P & WithHelperProps>, keyof WithHelperProps> & { ref: RefObject<InstanceType<V>>; onLayout: () => void; }' is not assignable to type 'LibraryManagedAttributes<V, Readonly<P>>'.

I've been playing with it for a while and searching around, but I can't figure out what I'm doing wrong here.


I made a minimal reproduction in Replit.

1 Answers

I think you just forgot to forwardRef your component's ref, which is needed since the HOC will be wrapping another component. Here's a simplified version (I'll update to your specific use case in a moment) for the sake of demonstration.

interface Layout {
  height: number
  width: number
  x: number
  y: number
}

interface WithHelperProps {
  children?: React.ReactNode
  layoutParam: string
  onChangeLayout: (props: { layout: Layout, layoutParam: string }) => void
}

const withHelper = <T, P = {}>(
  Render: React.ComponentType<P & { ref?: React.Ref<T> }>,
) => {
  const HOC = React.forwardRef<T, P & WithHelperProps>(
    ({ children, ...props }, ref) => {
      return (
        <Render {...(props as P)} ref={ref}>
          {children}
        </Render>
      )
    },
  )
  return HOC
}

Hope that helps! Cheers

Related