Kevin De Baerdemaeker

Polymorphic Components with Typescript

Modified 2022-09-23

type VariantTypeStyles = keyof DefaultTheme['typography']
interface ITypographyProps<HTMLTag extends React.ElementType = React.ElementType>
  extends IDOMProps,
    IStyleProps {
  /**
   * Applies the theme typography styles.
   * @default 'body'
   */
  variant?: VariantTypeStyles
  /**
   * Change the semantic HTML element, while visually maintaining the style
   * of the variant
   */
  component?: HTMLTag
  /** The content of what needs to be marked up. */
  children: ReactNode
}
type TypographyProps<HTMLTag extends React.ElementType> = ITypographyProps<HTMLTag> &
  Omit<React.ComponentPropsWithoutRef<HTMLTag>, keyof ITypographyProps>

const defaultElement = 'span'
const variantMap = new Map<VariantTypeStyles, React.ElementType>([
  ['h1', 'h1'],
  ['h2', 'h2'],
  ['h3', 'h3'],
  ['subtitle', 'h6'],
  ['body', 'p'],
  ['strong', 'strong'],
  ['button', 'span'],
  ['pretitle', 'span'],
  ['small', 'span'],
])
/**
 * The Typography component is used to quickly apply a default set of font
 * styles in the application in order to keep the typography consistent.
 *
 * NOTE: It's completely okay to semantically need a different HTML element,
 * than what is by default used mapping from the variants. It's encouraged even!
 */
export function Typography<HTMLTag extends React.ElementType = typeof defaultElement>({
  variant = 'body',
  component,
  children,
  ...props
}: TypographyProps<HTMLTag>) {
  return (
    <StyledTypography
      variant={variant}
      as={component || (variantMap.get(variant) ?? defaultElement)}
      {...props}
    >
      {children}
    </StyledTypography>
  )
}