Introduction
In the process of developing this site, I encountered that I was not having consistency between the style in texts. So I decided to create a Material UI-like Typography component.
Material UI exposes an "as" prop in almost all components. This prop allows developers to customize the HTML tag that the component will render.
React facilitates the implementation of this property in an easy way using a variable, as shown below:
function Example() { const Component = 'h1' return <Component>This will render a h1</Component> }
The only challenge is to implement this property while maintaining type safety, because a <h1> tag does not have the same properties as an <a> tag.
How to Type the "as" Prop Using TypeScript
Firstly, we need to create a type that receives a generic E. This generic extends from React.ElementType, ensuring that E can only take valid HTML tag names.
type PolymorphicProps<E extends React.ElementType> = { as?: E }
Next, we wrap our definition using the utility types that React provides to complete the props for a specified element. Typically, we statically write the tag, for example React.ComponentPropsWithoutRef<'p'> , but since we are dealing with a dynamic tag, we pass the E type.
type PolymorphicProps<E extends React.ElementType> = React.PropsWithChildren< React.ComponentPropsWithoutRef<E> & { as?: E } >
Finally, we define the component Props using the PolymorphicProps type. Now, the component can receive an optional "as" prop that will default to a <p> tag, and the rest of properties will autocomplete. Custom properties can be added using intersection types, for example color.
type PolymorphicProps<E extends React.ElementType> = React.PropsWithChildren< React.ComponentPropsWithoutRef<E> & { as?: E } > type TypographyProps<T extends React.ElementType> = PolymorphicProps<T> & { color?: string } export function Typography<T extends React.ElementType = 'p'>({ as, color, ...props }: TypographyProps<T>) { const Component = as || 'p' console.log(color) return <Component {...props} /> }
This approach allows us to create a reusable component that has type safety, keeps consistency between styles, and is easy to reuse.
function Social() { return ( <section> <Typography as='h1' className='mb-4'> Connect </Typography> <Typography as='a' className='mb-4' href='https://www.christianvm.dev/' target='_blank' > Link to my website </Typography> </section> ) }