How to best type a TypeScript collection of React wrappers - reactjs

in my team's application, we have run into a few cases where it would be nice to be able to dynamically compose component wrappers (HOCs) without having to know all the wrapper interfaces ahead of time (mostly for swapping out context providers when large portions of our component tree are rendered from different host containers).
I'm able to write a simple implementation for this, but getting optimal type safety has been... challenging 😉. In short, I'd like to be able to declare a collection of wrappers such that when any wrapper components are passed into it, the compiler will enforce that each wrapper component gets its correct props type as well.
Here's some code to illustrate the problem. Essentially, my question is how to define WrapperSet.
import * as React from "react";
export const Container: React.FC = () => {
// these could be any arbitrary wrapper components
const wrappers: WrapperSet = [
[WrapperA, { label: "WrapperA" }],
[WrapperB, { title: "foo" }],
];
const content = <span>Original content</span>;
return wrap(content, wrappers);
};
const WrapperA: React.FC<{ label: string }> = ({ label, children }) => (
<>
<div>WrapperA: {label}</div>
{children}
</>
);
const WrapperB: React.FC<{ title: string }> = ({ title, children }) => (
<>
<div>WrapperB: {title}</div>
{children}
</>
);
function wrap(children: JSX.Element, wrappers: WrapperSet): JSX.Element {
let content = children;
wrappers.forEach((wrapper) => {
const [ComponentType, props] = wrapper;
content = <ComponentType {...props}>{content}</ComponentType>;
});
return content;
}
type WrapperSet = [React.ComponentType, React.PropsWithChildren<{}>][];
👆 If I hover over WrapperSet here, TS tells me type WrapperSet = [any, any][], which is not at all protective.
Some other definitions of WrapperSet I have tried:
type WrapperSet2 = [React.ComponentType<any>, React.PropsWithChildren<any>][] &
{
[K in number]: WrapperSet2[K] extends [React.ComponentType<infer P>, any]
? [React.ComponentType<P>, React.PropsWithChildren<P>]
: never;
};
👆 TS interprets this as type WrapperSet2 = [any, any][] & { [x: number]: [any, any]; }, which is no better.
type Wrapper<P> = [React.ComponentType<P>, React.PropsWithChildren<P>];
type PropsFromComponentType<T extends React.ComponentType<any>> =
T extends React.ComponentType<infer P> ? P : never;
type WrapperSet3 = [any, any][] & {
[K in number]: WrapperSet4[K] extends [React.ComponentType<infer C>, any]
? Wrapper<PropsFromComponentType<C>>
: never;
};
👆 TS says type WrapperSet3 = [any, any][] & { [x: number]: Wrapper<unknown>; }.
It seems like there must be some way to tell TS that the outer WrapperSet array supports any kinds of Wrappers, but each Wrapper must be internally consistent, based on a single props type. Or maybe TS doesn't support this kind of expression. FYI, I am using TS 4.3.
Thanks in advance!

You can do it with a combination of types and a utility function that will enforce the props for the given component:
type ComponentAndProps<C extends React.ElementType<any>> = [C, React.ComponentPropsWithRef<C>];
type WrapperSet = ComponentAndProps< React.ElementType<any> >[];
function makeGroup<C extends React.ElementType<any>>(Component: C, props: React.ComponentPropsWithRef<C>): ComponentAndProps<C> {
return [Component, props];
}
Here's a usage example:
export const Container: React.FC = () => {
// these could be any arbitrary wrapper components
const wrappers: WrapperSet = [
makeGroup(WrapperA, { label: "WrapperA" }),
makeGroup(WrapperB, { title: "foo" }),
makeGroup(WrapperB, {}), // Error: missing 'title' property
];
const content = <span>Original content</span>;
return wrap(content, wrappers);
};
The trick is the makeGroup() helper function that allows TypeScript to infer and enforce the props type for the component.
If you just use the tuple notation then the props end up as any and TypeScript can't enforce the props:
const wrappers2: WrapperSet = [
[WrapperA, { label: "WrapperA" }],
[WrapperB, {}] // BAD - no error, TypeScript can't infer the props type for the component here
];
Final note - React has a number of utility types for extracting the props type from a component.
I chose ComponentPropsWithRef just in case you have a component that uses refs, but adjust as necessary:
React.ComponentProps<Component>
React.ComponentPropsWithRef<Component>
React.ComponentPropsWithoutRef<Component>

You can create type util which will accept two components and produce a tuple of wrappers and corresponding props:
import React, { FC, ComponentProps } from "react";
type Wrap<Comps extends FC<any>[]> = {
[Comp in keyof Comps]: Comps[Comp] extends React.JSXElementConstructor<any>
? [Comps[Comp], ComponentProps<Comps[Comp]>]
: never
}
type WrapperSet = Wrap<[typeof WrapperA, typeof WrapperB]>
export const Container: React.FC = () => {
// these could be any arbitrary wrapper components
const wrappers: WrapperSet = [
[WrapperA, { label: "WrapperA" }],
[WrapperB, { title: "foo" }],
];
const content = <span>Original content</span>;
return wrap(content, wrappers);
}; // compiles
But there is still a problem with:
function wrap(children: JSX.Element, wrappers: WrapperSet): JSX.Element {
let content = children;
wrappers.forEach((wrapper) => {
const [ComponentType, props] = wrapper;
content = <ComponentType {...props}>{content}</ComponentType>;
});
return content;
}
Likewise, multiple candidates for the same type variable in contra-variant positions causes an intersection type to be inferred.
This is why ComponentType expects an intersection of all props and not a union. TS is unable to figure out which prop correspond to each component in dynamic loop.
In order to fix it, we need to create extra function:
const iteration = <Comp extends React.JSXElementConstructor<any>>(
Comp: Comp,
props: ComponentProps<Comp>,
content: JSX.Element
) => <Comp {...props} >{content}</Comp>
And the whole code:
import React, { FC, ComponentProps } from "react";
type Wrap<Comps extends FC<any>[]> = {
[Comp in keyof Comps]: Comps[Comp] extends React.JSXElementConstructor<any>
? [Comps[Comp], ComponentProps<Comps[Comp]>]
: never
}
type WrapperSet = Wrap<[typeof WrapperA, typeof WrapperB]>
export const Container: React.FC = () => {
// these could be any arbitrary wrapper components
const wrappers: WrapperSet = [
[WrapperA, { label: "WrapperA" }],
[WrapperB, { title: "foo" }],
];
const content = <span>Original content</span>;
return wrap(content, wrappers);
};
const WrapperA: React.FC<{ label: string }> = ({ label, children }) => (
<>
<div>WrapperA: {label}</div>
{children}
</>
);
const WrapperB: React.FC<{ title: string }> = ({ title, children }) => (
<>
<div>WrapperB: {title}</div>
{children}
</>
);
const iteration = <Comp extends React.JSXElementConstructor<any>>(
Comp: Comp,
props: ComponentProps<Comp>,
content: JSX.Element
) => <Comp {...props} >{content}</Comp>
function wrap(children: JSX.Element, wrappers: WrapperSet) {
let content = children;
wrappers.forEach((wrapper) => {
const [ComponentType, props] = wrapper;
content = iteration(ComponentType, props, content)
});
return content
}
Playground

Related

How to define types for generic react Component?

I struggle to figure out how to type things properly.
I have a generic IPost type declared as:
export enum PostKind {
Message = "message",
Food = "food",
}
export type IPost<T extends PostKind> = {
type: T;
content: PostContent<T>; // not including the code for this type since is not relevant for now.
};
I have specific post types that extend this type:
export type IMessagePost = IPost<PostKind.Message> & {
messageLength: number;
};
export type IFoodPost = IPost<PostKind.Message> & {
image: string;
};
I have components for each specific post type:
export const MessagePost = (props: IMessagePost) => {
return <div>{props.messageLength}</div>;
};
export const FoodPost = (props: IFoodPost) => {
return <div>{props.image}</div>;
};
All good so far. New I want a generic Post component that takes an IPost param and displays the correct component that matches the PostKind.
const componentMap: Record<
PostKind,
React.FC<IMessagePost> | React.FC<IFoodPost>
> = {
[PostKind.Message]: MessagePost,
[PostKind.Food]: FoodPost,
};
export const Post = (props: IPost<PostKind>) => {
const Component = componentMap[props.type];
return <Component {...props} />; // typescript error here: Type 'PostKind' is not assignable to type 'PostKind.Message'
};
Something is worng with my types, and I cannot find a proper solution. Typescript shows the following error:
Type '{ type: PostKind; }' is not assignable to type 'IPost<PostKind.Message>'.
Types of property 'type' are incompatible.
Type 'PostKind' is not assignable to type 'PostKind.Message'.
You can check the full code, and see the error here:
Please don't suggest solution that uses types like unknown any ElementType ReactNode, or using the as keyword. I want everything to be typesafe.
So, I think in order for it to be sensible, the definition should be like this, right?
export type IFoodPost = IPost<PostKind.Food> & {
image: string;
};
Then, you can replace the last part with this:
const componentMap = {
[PostKind.Message]: MessagePost,
[PostKind.Food]: FoodPost,
};
export const Post1 = <A extends IPost<B>, B extends PostKind & keyof C, C extends Record<B, (a: A) => JSX.Element>>(props: A, rec: C) => {
const Component = rec[props.type]
return React.createElement(Component, props)
};
export const Post = <A extends IPost<PostKind>>(props: A) => {
return Post1(props, componentMap)
};
Given that, the <Post /> component will work if you type it explicitely:
const c1 = <Post<IMessagePost> type={PostKind.Message} messageLength={88} />
const c2 = <Post<IFoodPost> type={PostKind.Food} image={"xxxx"} />
const c3 = <Post<IFoodPost> type={PostKind.Food} /> // fails to typecheck
const c4 = <Post<IFoodPost> type={PostKind.Food} messageLength={5} /> // fails to typecheck

Fixing types for mapping over object to create React components

I really like uing this pattern for rendering similar components and using a _type prop to distinguish it and pass it down to the correct component.
However, I've found it difficult to add in the types correctly and was wondering if you guys could help. I have some questions;
Is the BlockMap type correct?
What type should I be using for ResolvedBlock?
Or generally, is there a better way of writing the types (without changing this structure?)
import React from 'react'
import { ImageBlock } from '/ImageBlock' // Assume all components are imported from whereever
type BlockType =
| 'imageBlock'
| 'formBlock'
| 'statisticBlock'
| 'videoBlock'
| 'quoteBlock'
interface Block {
_type: BlockType
_key: string
heading?: string
backgroundColor?: string
theme?: 'dark' | 'light'
}
type BlockMap = Record<BlockType, JSX.Element> // Is this type correct?
const blockMap:BlockMap = {
imageBlock: ImageBlock,
formBlock: FormBlock,
statisticBlock: StatisticBlock,
videoBlock: VideoBlock,
quoteBlock: QuoteBlock,
}
interface Props {
className?: string
blocks: Block[]
}
export function BlocksBuilder({
blocks = [],
className = ``,
}: Props):JSX.Element {
return (
<>
{blocks.map(block => {
const ResolvedBlock = blockMap[block._type] // What type should ResolvedBlock be?
if (!ResolvedBlock) return null
return (
<ResolvedBlock
className={className}
block={block}
key={block._key}
/>
)
})}
</>
)
}
It's a good pattern, and your type is close, but you will want to define the shape of your components instead.
type BlockMap = Record<BlockType, (props: any) => JSX.Element>
You could also define the props for Block components, and use that type for each of your components
interface BlockProps {
key: string;
block: Block;
classname: string;
}
type BlockComponent = (props: BlockProps) => JSX.Element;
export const ImageBlock: BlockComponent = (props) => {
return <></>;
};
type BlockMap = Record<BlockType, BlockComponent>

incompatible function argument in component props

I have a component that takes a list of items, known to have an ID, and a function that filters those items.
The type with an ID is a generic type of item, that all items will have.
But more specific items will include other props.
type GenericItem = {
id: string;
}
type SpecificItem = {
id: string;
someOtherProp: boolean;
}
I also have a function type, that uses the generic type to operate on.
type GenericItemFunction = (item: GenericItem) => boolean;
I then have this component that uses the GenericItem and GenericItemFunction in its props.
type CompProps = {
fn: GenericItemFunction;
items: GenericItem[];
}
const Comp: React.FC<CompProps> = ({ fn, items }) => <></>;
When I try to use this component with the Specific type, I am getting errors saying I cannot use an implementation of GenericItemFunction because the types for item are not compatible.
const App = () => {
const items: SpecificItem[] = [];
const filter = (item: SpecificItem) => item.someOtherProp;
return (
<Comp
fn={filter} // error on `fn` prop
items={items}
/>
)
}
The typescript error I receive is:
Type '(item: SpecificItem) => boolean' is not assignable to type 'GenericItemFunction'.
Types of parameters 'item' and 'item' are incompatible.
Property 'someOtherProp' is missing in type 'GenericItem' but required in type 'SpecificItem'.
I guess I have two questions;
Firstly, Why is there a conflict when both types expect the id: string property?
Secondly, is there a more sane way to do something like this?
My first though was the type for item on the GenericItemFunction could be inferred from the value provided to the items prop in the App component.
But to be completely honest, I'm not sure how that would look...
My other thought was to have the Comp be a generic, but not show to use a react component that uses generics... Seems like jsx/tsx doesn't really support that syntax.
I expect something like this to throw all sorts of errors.
const Comp = <T extends GenericItem,>({ fn, items }) => <></>;
const App = () => {
return <Comp<SpecificType> />;
}
Finally, I did try this and there aren't any errors. But the downside is the type for items is inferred to be any.
type GenericItem = {
id: string;
}
type SpecificItem = {
id: string;
someOtherProp: boolean;
}
type GenericItemFunction <T> = (item: T) => boolean;
type CompProps <T extends GenericItem = any> = {
fn: GenericItemFunction<T>;
items: T[];
}
const Comp: React.FC<CompProps> = ({ fn, items }) => <></>;
const App = () => {
const items: SpecificItem[] = [];
const filter = (item: SpecificItem) => item.someOtherProp;
return (
<Comp
fn={filter}
items={items}
/>
)
}
Here's a link to the playground I've been using.
https://www.typescriptlang.org/play?#code/C4TwDgpgBA4hB2EBOBLAxgSWBAtlAvFAN4BQAkCgCYBcUAzsKvAOYDcJAviSaJFAMqQ0KAGbosuAsRJRZUKrQZM2MuXQD2OCAHlgAC2QAFJOrC0ARuvUAbCAEN47Lj3DQ4iVJmw4AYgFd4NGAUdXgoAB4AFQA+KQAKFG9aSIBKAljLG3tHbhc+AGFNMGNTOgjIqAgAD2x4SjL3ZHFvKQcQWMJSMhF4WkbPCV8AoJD4KOj2OXlvOmSAbQBdJxI0UIYoQpwzKAAleyCAOh988M3ikzA6Dqg4oigegBpp3DKONPxY8OjwgHoJ7lW8HWAEEwGB4u9YqQpoD1okXrRBBBhGIvLhFlJFpM5LDgPcUNZsEh4vCcIihKJmrhIc8cAcNFpdAYkCUwOxVLIkBBgH4kGE4hyphEzoKhXIevgiGJCcguGKxaS6JLFXL5X9BSlOEA
UPDATE:
Why did you use any as default for GenericItem type? Without this I believe it should properly infer Genericitem from GenericItemFunction. – tymzap
Removing the = any for the CompProps typedef causes errors in the Comp declaration...
type CompProps <T extends GenericItem> = {
fn: GenericItemFunction<T>;
items: T[];
}
const Comp: React.FC<CompProps> = ({ fn, items }) => <></>; // this line has the error
Generic type 'CompProps' requires 1 type argument(s).
Meaning, I still have to declare the type somewhere. Meaning I need to know the variation of the GenericItem type before I use the component.
SpecificItem is just a representation of the types that happen to overlap with the GenericItem typedef.
In most cases, the Comp wont know what type will actually be used and any doesn't give any useful information to the author.
I'm hoping for something like...
type CompProps <T extends GenericItem> = {
items: <T extends GenericItem>[];
fn: <infer from T>[];
}
But I'm not sure if this exists, or something like it.
:face_palm:
The CompProps using <T extends GenericType = any> is the correct way to do this.
type CompProps <T extends GenericItem = any> = {
items: T[];
filter: (item: T) => boolean;
}
const Comp: React.FC<CompProps> = (props) => <></>;
The magic is happening here:
const App = () => {
...
const filter = (item: SpecificItem) => item.someOtherProp;
...
}
The const Comp: React.FC<CompProps> doesn't care what the generic type is, hence = any. As long as it has at least the same shape as GenericType.
But inside the App component, where we are defining the method for the filter prop, we are declaring the SpecificItem typedef in the argument. Only that method uses the SpecificItem typedef. CompProps only cares it meets the required shape of GenericItem.
Hope that helps someone.
In most cases, the Comp wont know what type will actually be used and any doesn't give any useful information to the author.
To me, that means Comp is generic. So I would remove the = any on CompProps:
type CompProps<T extends GenericItem> = {
fn: GenericItemFunction<T>;
items: T[];
};
...and then define Comp as generic. There's probably a way to do that with React.FC, but personally I don't use React.FC so I don't know what it is. :-) React.FC doesn't do much of anything for you, and as some people point out, amongst other things it says your component accepts children whether it does or not. I prefer to be explicit about it when my components accept children.
So I'd define Comp like this:
const Comp = <T extends GenericItem>({ fn, items }: CompProps<T>) => {
// Just to show that `fn` and `items` play together:
const example = items.map(fn);
return <>{example}</>;
};
If you wanted it to have children, you can add them easily enough. Use string if you only want to support text, or React.Node if you want to allow just about anything:
// Does allow children:
type Comp2Props<T extends GenericItem> = {
fn: GenericItemFunction<T>;
items: T[];
children: React.ReactNode;
};
const Comp2 = <T extends GenericItem>({ fn, items, children }: Comp2Props<T>) => {
// Just to show that `fn` and `items` play together:
const example = items.map(fn);
return <>{example}</>;
};
Either way, it works well in your scenario:
const App = () => {
const items: SpecificItem[] = [];
const filter = (item: SpecificItem) => item.someOtherProp;
return <>
<Comp
fn={filter}
items={items}
/>
</>;
};
And the types mean it doesn't work in places you probably don't want it to:
const App2 = () => {
const genericItems: GenericItem[] = [];
const filterGeneric = (item: GenericItem) => item.id === "example"; // Silly definition, but...
const specificItems: SpecificItem[] = [];
const filterSpecific = (item: SpecificItem) => item.someOtherProp;
return <>
{/* Desirable error, `fn` can't handle `GenericItem`s, only `SpecificItem`s */}
<Comp
fn={filterSpecific}
items={genericItems}
/>
{/* Works, `fn` handles the supertype of the items, which is fine */}
<Comp
fn={filterGeneric}
items={specificItems}
/>
{/* Desirable error because `Comp` doesn't [now] support `children` */}
<Comp
fn={filterSpecific}
items={specificItems}
>
children here
</Comp>
{/* Works because `Comp2` does support `children`*/}
<Comp2
fn={filterSpecific}
items={specificItems}
>
children here
</Comp2>
</>;
};
Playground link

Type is not assignable to type LibraryManagedAttributes

I am trying to create array field component that will accept any React Functional component that has BaseProps. However I get an error when rendering Component in ArrayField.
Please see code below. Any ideas what's wrong here?
type BaseProps<T> = {
name: string;
convertValue?: (value: T) => T;
};
type CompanyType = {
address: string;
employees: number;
};
type CompanyProps = BaseProps<CompanyType> & {
required?: boolean;
};
const Company = (props: CompanyProps) => {
return <div>{/** */}</div>;
};
type ArrayFieldProps<T, V extends React.FC<BaseProps<T>>> = {
Component: V;
componentProps: React.ComponentProps<V>;
values: T[];
};
const ArrayField = <T, V extends React.FC<BaseProps<T>>>({
Component,
values,
componentProps
}: ArrayFieldProps<T, V>) => {
return (
<React.Fragment>
{values.map((_, index) => (
<Component key={index} {...componentProps} />
))}
</React.Fragment>
);
};
export const App = () => {
const companies: CompanyType[] = [];
return (
<ArrayField
values={companies}
Component={Company}
componentProps={{
name: 'company',
convertValue: (value) => ({
...value,
address: value.address.toUpperCase()
}),
required: true
}}
/>
);
};
I would do it slightly differently. Component types are quite complex so IMO it's easier to reason about simpler types, and it this scenario it solves your problem. Instead of using the Component as a "base" for your interface, use props. Like this:
type BaseProps<T> = {
name: string;
convertValue?: (value: T) => T;
};
type ArrayFieldProps<T, P extends BaseProps<T>> = {
Component: React.ComponentType<P>;
componentProps: P;
values: T[];
};
const ArrayField = <T, P extends BaseProps<T>>({
Component,
values,
componentProps
}: ArrayFieldProps<T, P>) => {
return (
<>
{values.map((_, index) => (
<Component key={index} {...componentProps} />
))}
</>
);
};
So as you can see the main difference is that the second generic type has to extend BaseProps<T> instead of a component type with specific props (this is most likely where TypeScript gives up and it results in problems with key prop) and ultimately you want your Component to be any valid React component (whether it's class or a function one). Of course if you really insist on enforcing function components you can change React.ComponentType to React.FC and it would still work.
You have to take into consideration the
key={index}
portion because there is a mismatch from the expected type and what is passed. This portion is not included in any of the types and I guess typescript just interprets it as value to be passed (not actual key).
You may try to move it on outer div just to see if the situation improves.

React with Typescript -- Generics while using React.forwardRef

I am trying to create a generic component where a user can pass the a custom OptionType to the component to get type checking all the way through. This component also required a React.forwardRef.
I can get it to work without a forwardRef. Any ideas? Code below:
WithoutForwardRef.tsx
export interface Option<OptionValueType = unknown> {
value: OptionValueType;
label: string;
}
interface WithoutForwardRefProps<OptionType> {
onChange: (option: OptionType) => void;
options: OptionType[];
}
export const WithoutForwardRef = <OptionType extends Option>(
props: WithoutForwardRefProps<OptionType>,
) => {
const { options, onChange } = props;
return (
<div>
{options.map((opt) => {
return (
<div
onClick={() => {
onChange(opt);
}}
>
{opt.label}
</div>
);
})}
</div>
);
};
WithForwardRef.tsx
import { Option } from './WithoutForwardRef';
interface WithForwardRefProps<OptionType> {
onChange: (option: OptionType) => void;
options: OptionType[];
}
export const WithForwardRef = React.forwardRef(
<OptionType extends Option>(
props: WithForwardRefProps<OptionType>,
ref?: React.Ref<HTMLDivElement>,
) => {
const { options, onChange } = props;
return (
<div>
{options.map((opt) => {
return (
<div
onClick={() => {
onChange(opt);
}}
>
{opt.label}
</div>
);
})}
</div>
);
},
);
App.tsx
import { WithoutForwardRef, Option } from './WithoutForwardRef';
import { WithForwardRef } from './WithForwardRef';
interface CustomOption extends Option<number> {
action: (value: number) => void;
}
const App: React.FC = () => {
return (
<div>
<h3>Without Forward Ref</h3>
<h4>Basic</h4>
<WithoutForwardRef
options={[{ value: 'test', label: 'Test' }, { value: 1, label: 'Test Two' }]}
onChange={(option) => {
// Does type inference on the type of value in the options
console.log('BASIC', option);
}}
/>
<h4>Custom</h4>
<WithoutForwardRef<CustomOption>
options={[
{
value: 1,
label: 'Test',
action: (value) => {
console.log('ACTION', value);
},
},
]}
onChange={(option) => {
// Intellisense works here
option.action(option.value);
}}
/>
<h3>With Forward Ref</h3>
<h4>Basic</h4>
<WithForwardRef
options={[{ value: 'test', label: 'Test' }, { value: 1, label: 'Test Two' }]}
onChange={(option) => {
// Does type inference on the type of value in the options
console.log('BASIC', option);
}}
/>
<h4>Custom (WitForwardRef is not generic here)</h4>
<WithForwardRef<CustomOption>
options={[
{
value: 1,
label: 'Test',
action: (value) => {
console.log('ACTION', value);
},
},
]}
onChange={(option) => {
// Intellisense SHOULD works here
option.action(option.value);
}}
/>
</div>
);
};
In the App.tsx, it says the WithForwardRef component is not generic. Is there a way to achieve this?
Example repo: https://github.com/jgodi/generics-with-forward-ref
Thanks!
Creating a generic component as output of React.forwardRef is not directly possible 1 (see bottom). There are some alternatives though - let's simplify your example a bit for illustration:
type Option<O = unknown> = { value: O; label: string; }
type Props<T extends Option<unknown>> = { options: T[] }
const options = [
{ value: 1, label: "la1", flag: true },
{ value: 2, label: "la2", flag: false }
]
Choose variants (1) or (2) for simplicity. (3) will replace forwardRef by usual props. With (4) you globally chance forwardRef type definitions once in the app.
Playground variants 1, 2, 3
Playground variant 4
1. Use type assertion ("cast")
// Given render function (input) for React.forwardRef
const FRefInputComp = <T extends Option>(p: Props<T>, ref: Ref<HTMLDivElement>) =>
<div ref={ref}> {p.options.map(o => <p>{o.label}</p>)} </div>
// Cast the output
const FRefOutputComp1 = React.forwardRef(FRefInputComp) as
<T extends Option>(p: Props<T> & { ref?: Ref<HTMLDivElement> }) => ReactElement
const Usage11 = () => <FRefOutputComp1 options={options} ref={myRef} />
// options has type { value: number; label: string; flag: boolean; }[]
// , so we have made FRefOutputComp generic!
This works, as the return type of forwardRef in principle is a plain function. We just need a generic function type shape. You might add an extra type to make the assertion simpler:
type ForwardRefFn<R> = <P={}>(p: P & React.RefAttributes<R>) => ReactElement |null
// `RefAttributes` is built-in type with ref and key props defined
const Comp12 = React.forwardRef(FRefInputComp) as ForwardRefFn<HTMLDivElement>
const Usage12 = () => <Comp12 options={options} ref={myRef} />
2. Wrap forwarded component
const FRefOutputComp2 = React.forwardRef(FRefInputComp)
// ↳ T is instantiated with base constraint `Option<unknown>` from FRefInputComp
export const Wrapper = <T extends Option>({myRef, ...rest}: Props<T> &
{myRef: React.Ref<HTMLDivElement>}) => <FRefOutputComp2 {...rest} ref={myRef} />
const Usage2 = () => <Wrapper options={options} myRef={myRef} />
3. Omit forwardRef alltogether
Use a custom ref prop instead. This one is my favorite - simplest alternative, a legitimate way in React and doesn't need forwardRef.
const Comp3 = <T extends Option>(props: Props<T> & {myRef: Ref<HTMLDivElement>})
=> <div ref={myRef}> {props.options.map(o => <p>{o.label}</p>)} </div>
const Usage3 = () => <Comp3 options={options} myRef={myRef} />
4. Use global type augmentation
Add following code once in your app, perferrably in a separate module react-augment.d.ts:
import React from "react"
declare module "react" {
function forwardRef<T, P = {}>(
render: (props: P, ref: ForwardedRef<T>) => ReactElement | null
): (props: P & RefAttributes<T>) => ReactElement | null
}
This will augment React module type declarations, overriding forwardRef with a new function overload type signature. Tradeoff: component properties like displayName now need a type assertion.
1 Why does the original case not work?
React.forwardRef has following type:
function forwardRef<T, P = {}>(render: ForwardRefRenderFunction<T, P>):
ForwardRefExoticComponent<PropsWithoutRef<P> & RefAttributes<T>>;
So this function takes a generic component-like render function ForwardRefRenderFunction, and returns the final component with type ForwardRefExoticComponent. These two are just function type declarations with additional properties displayName, defaultProps etc.
Now, there is a TypeScript 3.4 feature called higher order function type inference akin to Higher-Rank Types. It basically allows you to propagate free type parameters (generics from the input function) on to the outer, calling function - React.forwardRef here -, so the resulting function component is still generic.
But this feature can only work with plain function types, as Anders Hejlsberg explains in [1], [2]:
We only make higher order function type inferences when the source and target types are both pure function types, i.e. types with a single call signature and no other members.
Above solutions will make React.forwardRef work with generics again.
I discovered this question from reading this blog post, and I think there is a more straight-forward way of accomplishing this than the current accepted answer has proposed:
First we define an interface to hold the type of the component using something called a call signature in typescript:
interface WithForwardRefType extends React.FC<WithForwardRefProps<Option>> {
<T extends Option>(props: WithForwardRefProps<T>): ReturnType<React.FC<WithForwardRefProps<T>>>
}
Notice how the function signature itself is declared as generic, not the interface - this is the key to making this work. The interface also extends React.FC in order to expose some useful Component properties such as displayName, defaultProps, etc.
Next we just supply that interface as the type of our component, and without having to specify the type of the props, we can pass that component to forwardRef, and the rest is history...
export const WithForwardRef: WithForwardRefType = forwardRef((
props,
ref?: React.Ref<HTMLDivElement>,
) => {
const { options, onChange } = props;
return (
<div ref={ref}>
{options.map((opt) => {
return (
<div
onClick={() => {
onChange(opt);
}}
>
{opt.label}
</div>
);
})}
</div>
);
});
Sandbox link here
References:
https://stackoverflow.com/a/73795451/2089675

Resources