James Conkling
James Conkling

Reputation: 3673

Using React forwardRef with Typescript generic JSX arguments

Given the following typed React component using generic type arguments, how would I go about wrapping it in React's new forwardRef API?

type Props<T> = {
  forwardedRef?: Ref<HTMLInputElement>
  ...
}

class GenericComponent<T> extends Component<Props<T>> {
  ...
}

const ComponentWithRef = forwardRef<HTMLInputElement, Props<T>>((props, ref) => (
  <StringInput<T> {...props} forwardedRef={ref} />
))

The above approach has no way to define the T generic.

Upvotes: 25

Views: 18113

Answers (2)

user22880182
user22880182

Reputation:

I think there is a workaround:

type Props<T> = {
    ref?: MutableRefObject<HTMLDivElement | null>;
    someProp: string; 
    testType: T;
}

const TestComponent: <T extends any>(props: Props<T>) => ReactNode = (() => {
    // eslint-disable-next-line react/display-name
    return forwardRef(({someProp, testType}, ref: ForwardedRef<HTMLDivElement>) => {
        console.log(someProp, testType);
        return <div ref={ref}>testasd</div>;
    });
})();

and then:

type TestType = {
    name: string;
};

const SomePage: FC = () => {
    const someRef = useRef<HTMLDivElement | null>(null);
    return (
        <div>
            <TestComponent<TestType> someProp='OK' ref={someRef} testType={{name: 'test'}}/>
        </div>
    );
};

this compiles with no errors.

the type of testType is inferred correctly

the following gets an error:

<TestComponent<TestType> someProp='ERROR' ref={someRef} testType={'test'}/>

Type 'string' is not assignable to type 'TestType'.ts(2322)

which is expected.

Upvotes: 1

James Conkling
James Conkling

Reputation: 3673

So, to broaden the question some, this is really a question about preserving generic types in higher order functions. The following usage of forwardRef will properly typecheck (in 3.0.1)

const SelectWithRef = forwardRef(<Option extends string>(props: Props<Option>, ref?: Ref<HTMLSelectElement>) =>
  <Select<Option> {...props} forwardedRef={ref} />);

But, the Option generic is immediately resolved to string, rather than remaining as a generic. As such, the following does not typecheck

const onChange: (value: 'one' | 'two') => void = (value) => console.log(value);

<SelectWithRef<'one' | 'two'>
              ^^^^^^^^^^^^^^^ [ts] Expected 0 type arguments, but got 1
  value="a"
  options={['one', 'two']}
  onChange={onChange}
           ^^^^^^^^^^ [ts] Type 'string' is not assignable to type '"one" | "two"'
/>

The relevant issue is tracked in this Typescript issue ticket.

Upvotes: 27

Related Questions