Polymorphic Components with React & TypeScript

Learn how to properly type annotate a Polymorphic Component in Typescript

Muggle-born
3 min readJan 16, 2022

What is a Polymorphic Component?

A common pattern that is used when designing reusable components is to expose an as prop on the component. The as prop determines what Element the component will render as. A very basic example of a Polymorphic component is shown below:

const DesignSystemButton = (props) => {
const {as: Component = 'button', ...rest} = props;
return (
<Component {...rest} />
);
}

Such a simple component as the one shown above does not provide much benefit. However, imagine you had a more mature <DesignSystemButton /> component that supported: various color themes, accessibility, typography, and more. For components like those, an as prop can be very beneficial. An as prop would allow other engineering teams to use your DesignSystemButton as a: react-router link, an HTMLAnchorElement, and much more. For example:

import { Link } from 'react-router';
import DesignSystemButton from 'company/design-system'
// Example using react-router
<DesignSystemButton as={Link} to="/courses?sort=name" />
// Example use HTMLAnchorElement
<DesignSystemButton as='a' href='/about' />

Type Annotating a Polymorphic Component

Let’s look a simple example to understand the problem we’re trying to solve. Do you notice anything wrong with the code below?

<DesignSystemButton as='button' href='/about' />

The problem in the code above is that a button element does not have an href attribute. Worst of all, TypeScript did not catch this error!

We are going to annotate our <DesignSystemButton /> component so Typescript catches errors like the one above. Whatever element we pass in for the as prop should flag invalid additional props such as href in the example above.

Step 1: Making a Generic Type

First, we need to define a type that allows us to pass in any React Element. We can accomplish this by using a generic Element that is constrained by the React.ElementType type.

type PolymorphicProps<Element extends React.ElementType> = {
as?: Element;
};

Now we have a typed as prop which allows you to pass any built-in HTML elements or custom react components. The next step is to type the additional ...rest of the properties.

Step 2: Intersecting types with the generic Element

Include Omit<React.ComponentProps<Element>, ‘as’> & which allows you to take all the normal props of theElement. This gives you proper typing for ...rest, regardless of what Element you use.

type PolymorphicProps<Element extends React.ElementType> =   
Omit<React.ComponentProps<Element>, 'as'> & {
as?: Element;
};

Step 3: Making our PolymorphicProps type definition reusable

As a nicety to developers we’re updating our generic to include a Props this way developers can reuse our generic PolymorphicProps type.

type PolymorphicProps<Element extends React.ElementType, Props> = 
Props &
Omit<React.ComponentProps<Element>, 'as'> & {
as?: Element;
};

Step 4: Adding types to our React Component

Now that we’ve created our type for PolymorphicProps we can use it in our <DesignSystemButton /> component.

// User-defined Props for DesignSystemButton
type Props = {
myProp: number;
otherProp?: string;
};
const defaultElement = "button";
const DesignSystemButton = <Element extends React.ElementType =
typeof defaultElement>(props: PolymorphicProps<Element, Props>) => {
const { as: Component = defaultElement, ...rest } = props;
return <Component {...rest} />;
};

The End Result

Now that your component is fully typed, let’s take a look at a list of example usages of your component and how typescript can help you detect errors now.

Visit the code sandbox used in this article to play around with the code

Additional Resources

If you’re interested in reading more about Polymorphic components then checkout the resources listed below:

--

--

Muggle-born
Muggle-born

Written by Muggle-born

Hi, my name is Jeremiah. I build things on the web for fun. I enjoy figuring how and why things work the way they do, and I try teaching others along the way.

No responses yet