What are Polymorphic components?
If you've ever tried any React component libraries like Material UI, Mantine UI or Chakra UI.
You might have used the
as prop, which is available in most of the library components,
the general idea behind this prop is to allow the consumers of your library to pass a
custom element or another component that can be rendered instead of a hard-coded value.
There are plenty of confusing definitions out there when it comes to this topic, though the way I like to think about it is this:-
A polymorphic component is a design pattern that allows a component to act in more than one way depending on the UI requirements without breaking any accessibility or Type checking. So a
Buttonmight work as a Navigation-link or vice versa.
A key thing to note here is that it's possible to build them without Typescript, however it is rudimentary and even incomplete without the type checking.
Let's see an example of a polymorphic
At first glance, the
Text component looks super simple. All it takes is a tag name and
renders that element with the given content, so what gets rendered on the page is this
Button component is where things start to become a little tricky, and this is where we see the true power of polymorphism.
a both are semantically very different, eg. a button does not accept an
href attribute whereas an
a tag does.
Now let's build one from scratch.
You can use any frontend tooling like CRA, Snowpack or Vite
I will use Vite, to quickly create a new React + Typescript application with Vite I can do
Creating the Text component
Text component should be able do the following
- Accept an
- Type check props against attributes of the element passed in
- Accept a
weightprop to customise the font-weight of the element.
Here is how our basic
Text component would look like
if it's your first time using
type, no worries.
All is does is to take a
span and return an object type of all
attributes for that element.
Cool, the basic component stuff is done but! it's still not Polymorphic.
We will check if the as prop is provided, if it is then render
that as the Component
otherwise default to a regular
So now if we try it like this,
_3<Text as="a" href="http://sidwebworks">_3Hello world_3</Text>
Typescript screams at us with this weird error. Let me simplify it for you.
All it's trying to say is
href does not exists on
TextProps which is quite right as
span to the
ComponentPropsWithoutRef utility and spans don't have a
Let's fix this.
Improving type support for
The problem we now have is, when the
as prop changes, the value passed to
ComponentPropsWithoutRef is still hard-coded.
So we need some way to pass a dynamic value in there which changes as we update our
as prop value.
Luckily for us Typescript offers a feature
Generics which can help us here.
A Generic is essentially a type value which can be passed around to other types and functions, kind of like variables which only Types can access.
So let's Genericify our component :)
In order to do that, it would be better to have a utility type like a
PolymorphicPropsWithoutRef which takes 2 generic types
C (as prop) and
We're just making this utility so we can have some reusability in our code.
Writing a utility type
Umm so what the hell does this type even do? Let's break it down a bit.
In the above highlighted code, like the name says we create a type for the
as prop that takes in a generic
C which extends the
Extending other types like this is called as Generic constrains meaning it will only accept a value that satifies that given constrain.
Next, we create our main utility
PolymorphicPropsWithoutRef, which again takes in 2 generics
C for the ElementType and an optional type
P for custom props which we default to as an empty object.
In this type we make use of 3 other utility types and compose them together.
These utilities are :-
PropsWithChildren: comes from React, adds the
AsProp: we made this! it adds the
ComponentPropsWithoutRef: comes from React, adds the element's HTML attributes.
Nice! let's use it back in our Text Component.
Works like a charm! Lets add our
Adding a custom
So we will have some default font weights object that our component will accept, and then use the
keyof operator to extract a union type from that object.
Then we can pass it to our polymorphic utility type
Next we can merge the
style object coming from the props and add an
fontWeight key, which will be the
weight prop's value
Now you can use these same techniques to create other polymorphic components like