Quick Tip: Creating Polymorphic Components in TypeScript

By admin
In this quick tip, excerpted from Unleashing the Power of TypeScript,

Steve
PERSON

shows you how to use polymorphic components in

TypeScript
ORG

.

In my article Extending the Properties of an HTML Element in

TypeScript
ORG

, I told you that, over the course of building out a large application, I tend to end up making a few wrappers around components. Box is a primitive wrapper around the basic block elements in HTML (such as <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 need multiple variations of Box that are all basically the same. What we’d like to do is use Box but also be able to specify what it ought to be under the hood. A polymorphic component is a single adaptable component that can represent different semantic HTML elements, with

TypeScript
ORG

automatically adjusting to these changes.

Here’s an overly simplified take on a Box element inspired by

Styled Components
ORG

.

And here’s an example of a Box component from

Paste,
ORG


Twilio
PERSON

’s design system:

<Box as="article" backgroundColor="colorBackgroundBody" padding="space60"> Parent box on the hill side

<Box backgroundColor="colorBackgroundSuccessWeakest"
FAC

display="inline-block" padding="space40" > nested box

1
CARDINAL

made out of ticky tacky </Box> </Box>

Here’s a simple implementation that doesn’t have any pass through any of the props, like we did with

Button
PERSON

and

LabelledInputProps
PERSON

above:

import { PropsWithChildren } from ‘react’; type

BoxProps
ORG

= PropsWithChildren<{ as: ‘div’ | ‘section’ | ‘article’ | ‘p’; }>; const Box = ({ as, children }:

BoxProps
ORG

) => { const TagName = as || ‘div’; return <TagName>{children}</TagName>; }; export default Box;

We refine as to TagName , which is a valid component name in

JSX
ORG

. That works as far a React is concerned, but we also want to get

TypeScript
ORG

to adapt accordingly to the element we’re defining in the as prop:

import { ComponentProps } from ‘react’; type

BoxProps
ORG

= ComponentProps<‘div’> & { as: ‘div’ | ‘section’ | ‘article’ | ‘p’; }; const Box = ({ as, children }:

BoxProps
ORG

) => { const TagName = as || ‘div’; return <TagName>{children}</TagName>; }; export default Box;

I honestly don’t even know if elements like <section> have any properties that a <div> doesn’t. While I’m sure I could look it up, none of us feel good about this implementation.

But what’s that ‘div’ being passed in there and how does it work? If we look at the type definition for ComponentPropsWithRef , we see the following:

type ComponentPropsWithRef<T extends

ElementType
PERSON

> = T extends new ( props: infer P, ) => Component<any, any> ? PropsWithoutRef<P> & RefAttributes<

InstanceType
ORG

<T>> : PropsWithRef<ComponentProps<T>>;

We can ignore all of those ternaries. We’re interested in

ElementType
PERSON

right now:

type

BoxProps
ORG

= ComponentPropsWithRef<‘div’> & { as:

ElementType
PERSON

; };

Okay, that’s interesting, but what if we wanted the type argument we give to

ComponentProps
ORG

to be the same as … as ?

We could try something like this:

import {

ComponentProps
ORG

,

ElementType
PERSON

} from ‘react’; type

BoxProps
ORG

<E extends

ElementType
PERSON

> = Omit<ComponentProps<E>, ‘as’> & { as?: E; }; const Box = <E extends

ElementType
PERSON

= ‘div’>({ as, …props }: BoxProps<E>) => { const TagName = as || ‘div’; return <TagName {…props} />; }; export default Box;

Now, a Box component will adapt to whatever element type we pass in with the as prop.

We can now use our Box component wherever we might otherwise use a <div> :

<Box as="section"

className="flex
NORP

place-content-between w-full"> <Button className="button" onClick={decrement}> Decrement </Button> <Button onClick={reset}>Reset</Button> <Button onClick={increment}>Increment</Button> </Box>

You can see the final result on the polymorphic branch of the

GitHub
ORG

repo for this tutorial.

This article is excerpted from Unleashing the Power of TypeScript, available on

SitePoint Premium
ORG

and from ebook retailers.