25

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.

2
  • 1
    I don't see any way to do it either. Your best option may be to not use forwardRef and just have callers call StringInput directly, passing the forwardedRef prop. Note that generic components are unsound to begin with, so I wouldn't hope for full support for type checking their usage. Commented Aug 16, 2018 at 21:21
  • @MattMcCutchen - yeah, that is a worrisome edge case to using generics: specifically, that a class component instance could be backed by two different elements with different generic type annotations, when those elements share the same underlying component. If I understand correctly, this weakness is only exposed in stateful class components, and not stateless class or functional components that don't share state across renders. Still a valid type vulnerability exposed by how React handles the component lifecycle, though. Commented Aug 17, 2018 at 14:49

3 Answers 3

27

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.

Sign up to request clarification or add additional context in comments.

Comments

1

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.

Comments

0

A solution I was able to get working with the assistance of Copilot AI but tested and working by me in my application:

The Child Component: [receiving the ref]

Note the "as <TData, TOption>" at the end of the ChildComponent definition is the critical piece to make this work.

import { forwardRef, ForwardedRef } from 'react';

// Define the props interface with generics
interface ChildComponentProps<TData, TOption> {
    data: TData[];
    option?: TOption;
    onSelect?: (item: TData) => void;
}

export const ChildComponent = forwardRef(
    <TData, TOption>(
        {
            data,
            option,
            onSelect
        }: ChildComponentProps<TData, TOption>,
        ref: ForwardedRef<ComponentRef<TData>>
    ) => {
        // Component implementation...
        return <div>Component Content</div>;
    }
) as <TData, TOption>(
    props: ChildComponentProps<TData, TOption> & {
        ref?: ForwardedRef<ComponentRef<TData>>;
    }
) => JSX.Element;

The Parent Component usage of child: [passing in the ref]

interface User {
    id: number;
    name: string;
}
    
type Options = "small" | "large";
    
<ChildComponent<User, Options>
    ref={componentRef}
    data={users}
    option="small"
    onSelect={(user) => console.log(user.name)}
/>

The type for Parent Component's Ref is:

RefObject<any>

Which you can type as necessary, I didn't have my use case finalised to give you a well typed solution for that particular piece. If it is a generic Element type, use that. If it is something custom (special returned functions from Child) use that definition.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.