
Photo by Lautaro Andreani on Unsplash
Creating polymorphic components with React and TypeScript
Make your components accessible and customizable
Adding polymorphism to your React components will make them more versatile as consumers will be able to define the element rendered to the DOM
.
There are important steps to follow when building these components so let’s get started!
Why do we need polymorphic components?
You may have noticed in some libraries like (Material UI or Chakra UI) some components have the option to allow an as
or elementType
prop. This is important for customization but also to improve accessibility by allowing us to use HTML semantic elements when rendering our components.
Adding an as
prop to a React component
Let’s see how we could implement something similar to these libraries - we’ll start with a simple Text
React component.
const Text = ({ as, children, ...rest }) => {
const Component = as || "span";
return <Component {...rest}>{children}</Component>;
};
export default function App() {
return <Text>Text goes here</Text>; // Will render a span by default
}
So here’s our component. It accepts an as
prop and children which means we can customize it straight away - let’s try that.
<Text as="main">Text goes here</Text>;
<Text as="p">Text goes here</Text>;
<Text as="section">Text goes here</Text>;
<Text as="a" href="www.example.com">Text goes here</Text>
What is wrong with this implementation?
Works like a charm! Oh, but wait…
<Text as="p" href="www.example.com">
Text goes here
</Text>
Nothing is stopping us from passing an href
to a paragraph, but we shouldn’t be allowed to use invalid HTML attributes right?
Here comes TypeScript
This is where our best friend TypeScript comes in.
We need to ensure that the Text
component props are strongly typed so they can be statically checked during the development process.
The plan is to allow our as
prop to accept any valid HTML element and then somehow determine which HTML attributes are valid based on that same element (e.g. an a
tag should allow the href
attribute).
Creating our types
Let’s start with setting up a type for the as
prop.
import { ElementType } from "react";
type AsProp<T extends ElementType> = {
/**
* The HTML element used for the root node.
*/
as?: T;
};
We’re setting the as
prop as optional and using a generic type that will be determined by the value passed for as
.
Our type T
is restricted to types that extend ElementType
from React i.e. any valid HTML elements.
import { ComponentPropsWithoutRef } from "react";
ComponentPropsWithoutRef<T>
We also want to include all other props such as style
, className
, ARIA tags, etc. But we don’t need to include the ref
prop for passing through refs hence the use of ComponentPropsWithoutRef
.
Omit<ComponentPropsWithoutRef<T>, keyof Props<T>>
We also want to look at T
and remove any props that are already defined in Props
so we’ll use keyof
and Omit
to achieve this.
Props<T> & Omit<ComponentPropsWithoutRef<T>, keyof Props<T>>
This also allows us to merge the properties of Props
and T
by removing any duplicates.
Our combined types should look something like this:
type TextProps<T extends ElementType> = PropsWithChildren<AsProp<T>> &
Omit<ComponentPropsWithoutRef<T>, keyof AsProp<T>>;
Making our component polymorphic
Finally TextProps
has everything we need for Text
! Let’s put everything together and add the type to our Text
component:
import {
ElementType,
ComponentPropsWithoutRef,
PropsWithChildren
} from "react";
type AsProp<T extends ElementType> = {
/**
* The HTML element used for the root node.
*/
as?: T;
};
type TextProps<T extends ElementType> = PropsWithChildren<AsProp<T>> &
Omit<ComponentPropsWithoutRef<T>, keyof AsProp<T>>;
const Text = <T extends ElementType = "span">({
as,
children,
...rest
}: TextProps<T>) => {
const Component = as || "span";
return <Component {...rest}>{children}</Component>;
};
As our Props
definition is generic our component should also be generic (and in our case, we also default the generic value to span
).
The best thing about our setup is that now ...rest
is also strongly typed.
So this should work:
<Text as="a" href="www.google.com">
Text goes here
</Text>
But this should generate a typing error:
<Text as="main" href="www.google.com">
Text goes here
</Text>
And we now have a polymorphic component! I hope you found this useful and make sure to check out the full example.