How to Build Polymorphic React Components with TypeScript

Build polymorphic React components that render as any element with an as prop. Type the pattern with ElementType and generics in TypeScript.

5 min read

A polymorphic React component renders as whatever element the parent chooses through an as prop. Type it with a generic constrained to ElementType, so the element type flows into the props and ref automatically. The same component can be a button, a link, or a heading depending on the call site.

What a polymorphic component is

A normal Box always renders a div. A polymorphic Box lets the parent pick the tag:

App.tsxApp.tsx
function Box({ as = "div", children, ...rest }) {
  const Tag = as;
  return <Tag {...rest}>{children}</Tag>;
}

The parent writes as="button" and the Box renders a button instead of a div. The challenge is typing as so the rest props match the chosen element. A well-typed as prop also fixes the props and the ref for the chosen tag, which is where TypeScript earns its keep.

Constrain the as prop

The as prop must be a valid element type. ElementType from React covers both tags and components, which makes it the right upper bound for the generic.

App.tsxApp.tsx
type BoxProps<C extends React.ElementType> = {
  as?: C;
  children: React.ReactNode;
} & React.ComponentPropsWithoutRef<C>;

The generic C is constrained to ElementType. Intersecting with ComponentPropsWithoutRef of C means the accepted props depend on whichever element the caller picks. This intersection reuses the ComponentPropsWithoutRef utility for extracting element prop types.

The typed component

Put the pieces together and default the element to div:

App.tsxApp.tsx
function Box<C extends React.ElementType = "div">({
  as,
  children,
  ...rest
}: BoxProps<C>) {
  const Tag = as || "div";
  return <Tag {...rest}>{children}</Tag>;
}

Now a Box with as="button" gets button props, and one with as="a" gets anchor props. TypeScript checks the attributes against the chosen element at each call site.

Use it in a real component

A design system Button often needs to act as a link without duplicating styles. With Box, the parent picks the tag:

App.tsxApp.tsx
function App() {
  return (
    <>
      <Box as="button" type="submit">Save</Box>
      <Box as="a" href="/docs">Docs</Box>
    </>
  );
}

The first Box is a submit button and the second is a link. TypeScript checks each one against its chosen element, so a typo in an attribute fails at the call site. This keeps one styled component for many tags instead of a family of near-identical buttons.

When to reach for as

Use as when the same visual component must render as different semantic elements. A Button that can also act as a link is the classic case. The alternative is two components that share styles, which duplicates code.

Keep as optional and default it. Most callers get the common case, and only the exceptions pass a tag. Prefer two explicit components when the semantics genuinely differ, like a real link versus a button.

What to learn next

This pattern leans on generics. If the type parameter syntax is new, read the guide on typing generic components next.

Rune AI

Rune AI

Key Insights

  • A polymorphic component uses an as prop to pick the tag.
  • Constrain the generic with React.ElementType.
  • Intersect props with ComponentPropsWithoutRef.
  • Default the element to div when as is missing.
  • TypeScript checks props against the chosen element.
RunePowered by Rune AI

Frequently Asked Questions

What is a polymorphic React component?

A component that can render as different elements. The parent picks the tag through an as prop, and the component renders that tag with the matching props.

Why use ElementType to type the as prop?

ElementType is the React type for any valid tag or component. Constraining the generic to ElementType lets the props depend on whichever element the caller chooses.

How do I default the rendered element?

Default the generic to the string div, like C extends React.ElementType = 'div', and fall back to div inside the component when as is not provided.

Conclusion

A polymorphic component renders as the element its parent chooses through an as prop. Type it with a generic constrained to ElementType, and intersect with ComponentPropsWithoutRef so props follow the chosen element.