- React
- Type-Safe React: Components, Generics, and Hooks
- Advanced TypeScript: Typing Polymorphic "as" Props
Advanced TypeScript: Typing Polymorphic "as" Props
Typing polymorphic components requires advanced TypeScript generics that map the "as" prop to React.ElementType, combine custom props with React.ComponentPropsWithoutRef<E>, and omit prop collisions to ensure accurate HTML attribute validation and ref forwarding.
Key Learning Objectives
Construct a complete polymorphic prop type utility (`PolymorphicComponentProps<E, P>`).
Use `React.ElementType` and `React.ComponentPropsWithRef<E>` to dynamically infer HTML attributes.
Prevent prop collisions by using TypeScript `Omit<T, K>` on overlapping property names.
Forward refs safely with proper DOM element generic typing without type errors.
The Interview Problem & Context
Construct a production-ready, fully type-safe polymorphic component type system in TypeScript that validates HTML attributes based on the "as" prop and supports ref forwarding.
1import React, { ElementType, ComponentPropsWithRef, forwardRef } from 'react';23// 1. Core Polymorphic Prop Utilities4type AsProp<E extends ElementType> = {5 as?: E;6};78// Merge custom props with native element props while omitting duplicates9export type PolymorphicProps<E extends ElementType, P = {}> = P &10 AsProp<E> &11 Omit<ComponentPropsWithRef<E>, keyof P | 'as'>;1213// 2. Custom Component Props14interface TextBaseProps {15 variant?: 'heading' | 'body' | 'caption';16 color?: string;17 children: React.ReactNode;18}1920// 3. Polymorphic Text Component Type with Ref Forwarding21export type TextComponent = <E extends ElementType = 'span'>(22 props: PolymorphicProps<E, TextBaseProps>23) => React.ReactElement | null;2425// 4. Implementation26export const Text: TextComponent = forwardRef(function Text<E extends ElementType = 'span'>(27 { as, variant = 'body', color, children, ...restProps }: PolymorphicProps<E, TextBaseProps>,28 ref: any29) {30 const Component = as || 'span';31 return (32 <Component ref={ref} className={`text-${variant}`} style={{ color }} {...restProps}>33 {children}34 </Component>35 );36});3738// 5. Compile-time Type Validation Examples:39export function Usage() {40 return (41 <>42 {/* VALID: <h1> tag with standard heading attributes */}43 <Text as="h1" variant="heading">44 Page Title45 </Text>4647 {/* VALID: <a> tag with href and target */}48 <Text as="a" href="https://example.com" target="_blank">49 Click Link50 </Text>5152 {/* COMPILE ERROR: <span href="..."> href does not exist on HTMLSpanElement! */}53 {/* <Text href="https://error.com">Invalid</Text> */}54 </>55 );56}
Deep Architectural Explanation
Deconstructing Polymorphic TypeScript Types
Building a polymorphic component requires solving three TypeScript challenges:
1. Dynamic Attribute Inference:
When a user writes <Text as="a" />, TypeScript must know that href and target are valid props, but when they write <Text as="button" />, type="submit" is valid and href is an error!
E extends React.ElementTypecaptures any valid HTML tag name ("div","a","button") or custom React component.React.ComponentPropsWithRef<E>fetches all valid props for that element.
2. Avoiding Prop Collisions with Omit:
If your custom component has a prop called color (e.g. color?: string), and HTML attributes also have a legacy color prop, TypeScript throws a union type mismatch unless you explicitly Omit conflicting keys:
Omit<ComponentPropsWithRef<E>, keyof P | 'as'>3. Default Generic Tag:
Setting <E extends ElementType = 'span'> ensures that if the as prop is omitted, the component safely defaults to <span> attributes.
Common Interview Traps & Pitfalls
Not using `Omit`, resulting in impossible type intersections when custom prop names collide with HTML attributes.
Using `React.ComponentType` instead of `React.ElementType`, which disallows native HTML string tags like "button" and "div".
Forgetting to provide a default generic type (`= "span"`), forcing consumers to always specify the `as` prop explicitly.
Senior Interviewer Follow-Up Probes
How does Radix UI`s `Slot` component circumvent the complex TypeScript typing of polymorphic `as` props?
How do you type polymorphic ref forwarding strictly without using `ref: any`?
How do you ensure polymorphic components remain performant in large design systems?
Frequently Asked Interview Questions
Why do we need Omit in polymorphic prop types?
To prevent type collisions between custom props and native HTML attributes. If both define a property with different types, TypeScript will create an impossible intersection (e.g., string & number = never) without Omit.
Why does forwardRef break generic component types in TypeScript?
React.forwardRef returns a ForwardRefExoticComponent which has a fixed, non-generic prop signature. You must cast the returned component using a custom generic type assertion.
What is the advantage of polymorphic components over multiple separate components?
It provides a unified, consistent design system API for typography, buttons, and layout containers, reducing code duplication while enforcing semantic HTML.
Can polymorphic components accept custom third-party components like Next.js Link?
Yes. Because ElementType includes React component types, you can pass as={Link} and TypeScript will infer Link's props (such as href and prefetch).
How do you test polymorphic components in React Testing Library?
Query elements by their semantic role (e.g., getByRole("heading") when as="h1" or getByRole("link") when as="a") to verify that the correct semantic DOM tag was rendered.
