This page looks best with JavaScript enabled

React polymorphic components with TypeScript

 ·  ☕ 4 min read  ·  ✍️ Iskander Samatov

Polymorphic components react and TypeScript


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 as prop.

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:

Polymorphic sample

Our button will render as a tag and accept the same props a tag accepts, like href.

Simple implementation

Now let’s get to the implementation. Without type checking, the pattern is rudimentary to implement:

Polymorphic component simple implementation

In our example above, we avoid any type checking by setting the component props type to any.

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:

misuse



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.

polymorphic component with type checking

The code above introduces generics. We made our component generic on the following line:

const Button = <T extends ElementType = "button">

ElementType 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.

We pass 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 . 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:

misuse error


The error message is convoluted, but the important part is: Property 'href' does not exist on type 'IntrinsicAttributes & MyButtonProps<"button">.

Our 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 :

polymorphic component with omit

Omit is a TypeScript utility that constructs a new type by excluding any prop keys in MyButtonProps from ComponentPropsWithoutRef. Using 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 .

Now our Button component is ready!

Conclusion

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.

Share on

Software Development Tutorials
WRITTEN BY
Iskander Samatov
The best up-to-date tutorials on web and mobile development.