29

As TypeScript improve its JSX type-checking in v3.2, we have a problem to correctly type our HOCs now.

Can someone fix types in the following HOC for TypeScript 3.2?

import { ComponentType } from 'react';

type Props = { custom: string };
type Omit<T, K extends string> = Pick<T, Exclude<keyof T, K>>;

function hoc<P extends Props>(Component: ComponentType<P>) {
  return (props: Omit<P, keyof Props>) => {
    return <Component {...props} custom="text" />;
  }
}

TypeScript error:

Type '{ custom: string; }' is not assignable to type 'IntrinsicAttributes & P & { children?: ReactNode; }'.
Property 'custom' does not exist on type 'IntrinsicAttributes & P & { children?: ReactNode; }'

Basically, the idea is to transform the component which requires "custom" property to component, which doesn't need it anymore as it will be injected automatically by HOC.

EDIT: Probably the same issue: https://github.com/Microsoft/TypeScript/issues/28748

Eran Shabi
  • 14,201
  • 7
  • 30
  • 51
Deftomat
  • 629
  • 6
  • 15

2 Answers2

2

I'm sure this is not the answer that you were hoping for, but you can make it work by changing the type of props in the inner function to any, and putting the Omit type in the return type annotation of the outer function, like this:

function hoc<P extends Props>(Component: ComponentType<P>): ComponentType<Omit<P, keyof Props>> {
  return (props: any) => {
    return <Component {...props} custom="text" />;
  }
}
Jesse Hallett
  • 1,857
  • 17
  • 26
  • 2
    I postponed the casting of `props` to `any` until the spread, so I used `{(...this.props as any)}`, so you can still work with the typed `props` elsewhere in the render function. So far, the most unintrusive solution. Thanks... – Ramon de Klein Dec 29 '18 at 14:45
  • The problem with this answer, is that you are returning a component will no typechecking on its props – dwjohnston Apr 17 '20 at 06:44
0

Here's an HoC that supports forwarding refs as well along with a jest test.

Ther is only one as and that's the part where I convert the input props to props used by the wrapped component since they can be different.

Note the T can be any because React-Native vs React JS differ on how to provide the constructor. What is the `T` type argument in ReactElement? is a related question as to what the T is.

import { render } from '@testing-library/react-native';
import { ComponentType, forwardRef, NamedExoticComponent, PropsWithoutRef, ReactElement, Ref, RefAttributes, useEffect, useRef } from 'react';
import { Text, TextProps } from 'react-native';

/**
 * This is a simple HoC that is a noop that supports ref forwarding.
 * @param Component component to wrap
 * @param options options for the HoC building
 * @typeParam P the exposed props of the higher order component
 * @typeParam Q the props for the wrapped component
 * @typeParam T type for ref attribute of the wrapped component
 * @typeParam O options for the HoC building
 * @returns A named exotic componentwith P props that accepts a ref
 */
function hoc<P, Q, T, O = {}>(Component: ComponentType<Q>, options?: O): NamedExoticComponent<PropsWithoutRef<P> & RefAttributes<T>> {
    function wrapped(props: P, ref: Ref<T>): ReactElement<Q> {
        // the an unknown as Q here is an example, but P and Q can be different.
        const componentProps: Q = props as unknown as Q;
        return <Component {...componentProps} ref={ref} />
    }
    const displayName =
        Component.displayName || Component.name || "AnonymousComponent";
    wrapped.displayName = displayName;
    return forwardRef(wrapped);
}

describe("hoc", () => {
    it("should work with text", () => {
        const HocText = hoc<TextProps, TextProps, Text>(Text);
        const { toJSON } = render(<HocText>simple string</HocText>);
        const { toJSON: expectedToJSON } = render(<Text>simple string</Text>)
        expect(toJSON()).toStrictEqual(expectedToJSON())
    });

    it("should pass ref for text", () => {
        const callback = jest.fn();
        const HocText = hoc<TextProps, TextProps, Text>(Text);
        function MyComponent() {
            const textRef = useRef<Text>(null);
            useEffect(() => {
                callback(textRef?.current)
            }, []);
            return <HocText ref={textRef}>simple string</HocText>
        }

        const { toJSON } = render(<MyComponent />);
        const { toJSON: expectedToJSON } = render(<Text>simple string</Text>)
        expect(toJSON()).toStrictEqual(expectedToJSON())
        expect(callback).toBeCalledTimes(1);
        expect(callback.mock.calls[0][0]).toBeTruthy();
    });

    it("should work the same way with normal Text", () => {
        const callback = jest.fn();
        function MyComponent() {
            const textRef = useRef<Text>(null);
            useEffect(() => {
                callback(textRef?.current)
            }, []);
            return <Text ref={textRef}>simple string</Text>
        }

        const { toJSON } = render(<MyComponent />);
        const { toJSON: expectedToJSON } = render(<Text>simple string</Text>)
        expect(toJSON()).toStrictEqual(expectedToJSON())
        expect(callback).toBeCalledTimes(1);
        expect(callback.mock.calls[0][0]).toBeTruthy();
    });


});
Archimedes Trajano
  • 35,625
  • 19
  • 175
  • 265