This quick tip is excerpted from Unleash the power of TypeScriptSteve shows you how to use polymorphic components in TypeScript.
In my article, Extending HTML Element Properties in TypeScript, I mentioned that in the process of building large applications, you tend to end up creating several wrappers around components. Box
A primitive wrapper around basic block elements in HTML, e.g. <div>
, <aside>
, <section>
, <article>
, <main>
, <head>
, and so on). But just as we don’t want to lose all the semantic meaning we get from these tags, we also don’t want multiple variations of the tags. Box
It’s all basically the same. What we want to do is Box
But you can also specify what’s inside.a polymorphic component is a single adaptable component that can represent a variety of semantic HTML elements, and TypeScript automatically adjusts to these changes.
I’m going to give you an overly simplistic view here. Box
Elements inspired by styled components.
And here is an example Box
Components of Paste, Twilio’s design system:
<Box as="article" backgroundColor="colorBackgroundBody" padding="space60">
Parent box on the hill side
<Box
backgroundColor="colorBackgroundSuccessWeakest"
display="inline-block"
padding="space40"
>
nested box 1 made out of ticky tacky
</Box>
</Box>
This is a simple implementation that, like before, does not pass through any properties. Button
and LabelledInputProps
On top of that:
import PropsWithChildren from 'react';
type BoxProps = PropsWithChildren< 'p';
>;
const Box = ( as, children : BoxProps) => 'div';
return <TagName>children</TagName>;
;
export default Box;
we are fine as
to TagName
, which is a valid component name in JSX. This works as far as React is concerned, but it also requires TypeScript to adapt depending on the element you are defining. as
Props:
import ComponentProps from 'react';
type BoxProps = ComponentProps<'div'> & 'article' ;
const Box = ( as, children : BoxProps) => ;
export default Box;
Honestly, I don’t even know if I like the elements <section>
It has this characteristic. <div>
I don’t. I’m sure you can look into it, but I don’t see anyone feeling good about this implementation.
But what is it? 'div'
is passed there, but how does it work? Looking at the type definition for ComponentPropsWithRef
we find the following:
type ComponentPropsWithRef<T extends ElementType> = T extends new (
props: infer P,
) => Component<any, any>
? PropsWithoutRef<P> & RefAttributes<InstanceType<T>>
: PropsWithRef<ComponentProps<T>>;
All three terms can be ignored.we are interested ElementType
right now:
type BoxProps = ComponentPropsWithRef<'div'> &
as: ElementType;
;
I see, that’s interesting. But what if you want to specify type arguments? ComponentProps
will be the same as… as
?
we did it Try something like:
import ComponentProps, ElementType from 'react';
type BoxProps<E extends ElementType> = Omit<ComponentProps<E>, 'as'> &
as?: E;
;
const Box = <E extends ElementType="div">( as, ...props : BoxProps<E>) =>
const TagName = as ;
export default Box;
Now, Box
The component is as
Props.
is now available. Box
Anywhere a component can be used, <div>
:
<Box as="section" className="flex place-content-between w-full">
<Button className="button" onClick=decrement>
Decrement
</Button>
<Button onClick=reset>Reset</Button>
<Button onClick=increment>Increment</Button>
</Box>
The final result is polymorphic
Branch of this tutorial’s GitHub repository.
This article is an excerpt from Unleash the power of TypeScriptavailable from SitePoint Premium and e-book retailers.