Matt PocockMatt is a well-regarded TypeScript expert known for his ability to demystify complex TypeScript concepts.
React's props model is extremely powerful. One of its most useful features is the ability to pass a component as a prop. This lets you create composable pieces of UI, helping to make your components more reusable.
The trouble is that this can often be difficult to type correctly. Let's fix that.
Here, we're passing <h1>My Site</h1> to the nav prop, and <div>Hello!</div> to the children prop.
We're typing our props as React.ReactNode, which is a type that accepts any valid JSX. Note that we're not using React.ReactElement or JSX.Element. I cover why in this article.
Here, we're typing the icon prop as React.ComponentType. We're passing { className?: string } to React.ComponentType, indicating that this is a component that can receive a className prop.
This basically says icon can be any component that can receive a className prop. This is a very flexible type, and it's easy to use.
Passing Any Component as a Prop and Inferring Its Props
The final method is to be able to receive any component and infer its props. This is very flexible but also extremely complex to type.
In my Advanced React and TypeScript course, I devote half of an entire section to this topic.
The final solution I landed on is documented here.
importReact, {ComponentPropsWithRef,ElementType,ForwardedRef,forwardRef,useRef,} from "react";typeFixedForwardRef = <T, P = {}>(render: (props: P,ref: React.Ref<T> ) =>React.ReactNode) => (props: P & React.RefAttributes<T>) =>React.ReactNode;constfixedForwardRef =forwardRef asFixedForwardRef;typeDistributiveOmit<T,TOmitted extendsPropertyKey> = T extends any ? Omit<T, TOmitted> : never;export constUnwrappedAnyComponent = <TAs extendsElementType>(props: {as?: TAs; } & DistributiveOmit<ComponentPropsWithRef<ElementType extendsTAs ? "a" : TAs >, "as" >,ref: ForwardedRef<any>) => { const { as: Comp = "a", ...rest } = props; return <Comp {...rest}ref={ref}></Comp>;};// Can be passed 'as' prop but defaults to 'a'constAnyComponent = fixedForwardRef(UnwrappedAnyComponent);// Defaulted to 'a'<AnyComponenthref="/" />;// It's now a div, so can't be an href!<AnyComponentas="div"href="/" />;
Type '{ as: "div"; href: string; }' is not assignable to type 'IntrinsicAttributes & { as?: "div" | undefined; } & Omit<DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "as"> & RefAttributes<...>'.
Property 'href' does not exist on type 'IntrinsicAttributes & { as?: "div" | undefined; } & Omit<DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "as"> & RefAttributes<...>'. Did you mean 'ref'?2322
Type '{ as: "div"; href: string; }' is not assignable to type 'IntrinsicAttributes & { as?: "div" | undefined; } & Omit<DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "as"> & RefAttributes<...>'.
Property 'href' does not exist on type 'IntrinsicAttributes & { as?: "div" | undefined; } & Omit<DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "as"> & RefAttributes<...>'. Did you mean 'ref'?
If you're in a situation where you can choose either of the above approaches, I would lean towards passing JSX as a prop.
It's not only easy to type (React.ReactNode) but also very performance-friendly. JSX passed to a component as a prop is not re-rendered when that parent component re-renders. This can be a huge performance boost.
But if you do need the other methods, then React.ElementType and React.ComponentType are both easy to type and easy to use.
If you can, stay away from using the open-ended 'as' prop. But if you do need it, then the description in my advanced course will help.