Zach Olivare - 2022 Oct 19
Allowing customization of the root node of a React component & maintaining correct prop typing

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:
But that's not all! You can also pass your own React component and have IT rendered as the root node:
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!!!
Over 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:
But replicating the strong typing of doing so has (until today) eluded me. But here is my solution:
Let's break this solution down. First, the props definition:
type MyComponentProps<C ...> - Declares a generic type, with a generic argument of C, which I chose to vaguely stand for "Component".<C extends React.ElementType> - Puts a type constraint on C, 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. So ComponentProps<typeof MyComponent> works just as well as ComponentProps<'div'>.{component?: C} - Declares the component prop itself, which must be of type C (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 the component prop 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. The C here 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 typeSo 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...
...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:
