A polymorphic component is a popular React pattern. Even if you’ve never heard of it, you most likely encountered it in code. In a nutshell, the pattern lets you specify which HTML tag to use for rendering your component using
The flexibility of the polymorphic component also makes it easy to misuse, and that’s where TypeScript can help us. Let’s see how we can use TypeScript and generics to write strongly typed polymorphic components.
Overview of polymorphic components
First, let’s see how you would use polymorphic components. Say you have a
Button component that you want to render as an HTML link. If
Button is a polymorphic component, you can write it like this:
Our button will render as
a tag and accept the same props
a tag accepts, like
Now let’s get to the implementation. Without type checking, the pattern is rudimentary to implement:
In our example above, we avoid any type checking by setting the component props type to
Here is the line that makes our pattern work:
const Component = as || "button";
We render our component using the value of the
as prop or use the
button tag as a fallback. And that’s all we need to make this implementation work.
The problem with this approach is that there is no mechanism to prevent the client from passing incorrect props:
We specified the
href property, which belongs to the
a tag, without setting our
as prop. Ideally, TypeScript would catch this bug immediately, and we would see an error.
Type checking with TypeScript
Our next step is to tighten up the prop type checking for our component.
The code above introduces generics. We made our component generic on the following line:
const Button = <T extends ElementType = "button">
is a utility type from React. We set our generic parameter
T to ElementType to ensure our button only accepts HTML tags and other React component types.
T to the
MyButtonProps type that stores the props we define manually. Within
MyButtonProps, we set the type of the
as prop to
T, which glues it all together.
The final type of the
Button props is
MyButtonProps<T> & ComponentPropsWithoutRef<T>. This type is a combination of the props we specified manually and
ComponentPropsWithoutRef is a type from React library that contains a basic set of props for React components.
At this point, our
Button component can dynamically calculate the props it accepts based on the value of
as. If we try our client example earlier, we will see an error like we should:
The error message is convoluted, but the important part is:
Property 'href' does not exist on type 'IntrinsicAttributes & MyButtonProps<"button">.
Button component no longer accepts the
href property because it doesn’t render itself as a link. If we add
as="a", the error goes away.
Our component looks good, but there’s one more thing to tweak. We want to make sure there are no name collisions between the props we specify manually and the props provided by
ComponentPropsWithoutRef. Having name collisions can cause confusing TypeScript warnings.
Fixing the error is simple: all we need to do is adjust the final type of the component’s props to use Omit :
Omit is a TypeScript utility that constructs a new type by excluding any prop keys in
Omit, we made sure to avoid name collision conflicts between the two types. Check out my post to learn more about
TypeScript utility types useful in React
Button component is ready!
In this post, we learned to write strongly typed polymorphic components using TypeScript and generics.
Using this approach and using TypeScript with React, in general, is definitely more work upfront. But it’s all worth it in the end because we improve our development experience by providing additional guarantees in our code.
Thank you for reading.