Polymorphic React components with TypeScript

Polymorphic components enable customization of the underlying element (e.g., button, a, or div) while preserving the correct type for ref and other props. The most common use case when this is handy is a button that could render a or abutton element depending on what props are passed.

Here’s an example of implementation

import React, { forwardRef, ElementType, ComponentPropsWithRef } from "react";

type Props<T extends ElementType> = {
  as?: T;
} & ComponentPropsWithRef<T>;

const Button = forwardRef(
  <T extends ElementType = "button">(
    { as: Component = "button", ...props }: Props<T>,
    ref: React.Ref<
      T extends keyof JSX.IntrinsicElements ? JSX.IntrinsicElements[T] : T
    >
  ) => {
    return (
      <Component {...props} ref={ref}>
        {props.children}
      </Component>
    );
  }
);

Button.displayName = "Button";

Usage example

import React from 'react';
import Button from './Button';

const App = () => {
  const buttonRef = React.useRef<HTMLButtonElement>(null);
  const anchorRef = React.useRef<HTMLAnchorElement>(null);

  return (
    <div>
      <Button
        ref={buttonRef}
        onClick={() => console.log('Button clicked')}
      >
        Button
      </PolymorphicButton>

      <Button
        as="a"
        ref={anchorRef}
        href="#"
        onClick={() => console.log('Anchor clicked')}
      >
        Anchor
      </Button>
    </div>
  );
};

export default App;

Conditional Types and Ref Handling

A key aspect of this implementation is the conditional type expression used for the ref prop.

React.Ref<T extends keyof JSX.IntrinsicElements ? JSX.IntrinsicElements[T] : T>

This expression checks if T is a valid built-in HTML element type. If it is, it retrieves the corresponding prop types for that built-in element. If it isn't, it assumes T represents a custom component type and keeps the type as T. This ensures that the ref prop has the correct type for built-in and custom components.


Click here to share this article with your friends on X if you liked it.