Polymorphic Components with React & TypeScript
Learn how to properly type annotate a Polymorphic Component in Typescript
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: