Allowing customization of the root node of a React component & maintaining correct prop typing

What is the “component” prop?
Section titled: What is the “component” prop?The Material UI React Library (MUI) introduced me a long time ago to this concept of a “component” prop. The idea is to allow the user of a component to customize the DOM node that is used to render that component. For example say a <List> component by default renders a <ul> to the DOM. But your use case for the list is a site navigation, so you want to render a <nav> as the root DOM node instead. With MUI, all you would have to do is:
<List component="nav">But that’s not all! You can also pass your own React component and have IT rendered as the root node:
function MyComponent(props: {children: ReactNode; foobar: 'foo' | 'bar'}) { const {children, foobar} = props return ( <div> <p>Zach is cool: {foobar}</p> {children} </div> )}
<List component={MyComponent} foobar='bar'>One of the beautiful things about doing so is that you can now pass any props that are specific to MyComponent to <List component={MyComponent}>, and they’ll get passed through correctly. Not only that! But the props are strongly typed when passed to the List!!!
Replicating the “component” prop
Section titled: Replicating the “component” propOver the years I have tried to replicate this component prop several times in component libraries that I’ve built. Replicating the JS functionality is simple enough, for example:
function MyComponent(props) { const {component: Component = 'div', ...rest} = props return <Component {...rest} />}But replicating the strong typing of doing so has (until today) eluded me. But here is my solution:
import React from 'react'
export type MyComponentProps<C extends React.ElementType> = React.ComponentProps<C> & { /** * The component used for the root node. * Either a string to use an HTML element or a component. */ component?: C}
export function MyComponent<C extends React.ElementType = 'div'>(props: MyComponentProps<C>) { const {component: Component = 'div', ...rest} = props return <Component {...rest} />}Let’s break this solution down. First, the props definition:
type MyComponentProps<C ...>- Declares a generic type, with a generic argument ofC, which I chose to vaguely stand for “Component”.<C extends React.ElementType>- Puts a type constraint onC, saying that it must be either a string of an HTML element (e.g. “div”, “a”, “input”, etc.), or a React component (e.g. MyComponent).React.ComponentProps<C>- This is where a lot of the magic happens. This clever predefined React type returns the type of props for any component type. SoComponentProps<typeof MyComponent>works just as well asComponentProps<'div'>.{component?: C}- Declares the component prop itself, which must be of typeC(which we earlier constrained to be either an HTML element string or a React component)
And now the component itself:
MyComponent<C extends React.ElementType ...>- Very similar to above, this declares that this React component is generic (yes, they can be generic), and its generic type must be either an HTML element string or a React component<C ... = 'div'>- Sets a default type for C for when thecomponentprop is not explicitly passed (this must match the default value in the component implementation)props: MyComponentProps<C>- The only use for a generic React component is to have generic props. TheChere would be invalid if it had not already been declared on the component function earlierconst {component: Component = 'div'} = props- Finally, destructure the component prop from the rest, rename it to have a capital letter so that it is legal to instantiate as a JSX component, and assign the same default value here in the implementation as we did in the generic type
Example usage
Section titled: Example usageSo now, if I create a simple component that accepts a prop named foobar that must be either “foo” or “bar”, and pass that as the component prop of MyComponent…
function Foobar(props: {foobar: 'foo' | 'bar'}) { return <div>{props.foobar}</div>}
return <MyComponent component={Foobar} />…I can add the foobar prop to MyComponent, and my IDE autocompletes the value because typescript KNOWS what it’s supposed to be!

And if I enter the wrong value, I get a type error:
return <MyComponent component={Foobar} foobar="zach" /> 