React Curriculum

Advanced TypeScript: Typing Polymorphic "as" Props

Hard100% Free~30 mins#react#typescript#polymorphic#as-prop#generics#type-manipulation
Interview Answer (30-Second Summary)

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';
2
3// 1. Core Polymorphic Prop Utilities
4type AsProp<E extends ElementType> = {
5 as?: E;
6};
7
8// Merge custom props with native element props while omitting duplicates
9export type PolymorphicProps<E extends ElementType, P = {}> = P &
10 AsProp<E> &
11 Omit<ComponentPropsWithRef<E>, keyof P | 'as'>;
12
13// 2. Custom Component Props
14interface TextBaseProps {
15 variant?: 'heading' | 'body' | 'caption';
16 color?: string;
17 children: React.ReactNode;
18}
19
20// 3. Polymorphic Text Component Type with Ref Forwarding
21export type TextComponent = <E extends ElementType = 'span'>(
22 props: PolymorphicProps<E, TextBaseProps>
23) => React.ReactElement | null;
24
25// 4. Implementation
26export const Text: TextComponent = forwardRef(function Text<E extends ElementType = 'span'>(
27 { as, variant = 'body', color, children, ...restProps }: PolymorphicProps<E, TextBaseProps>,
28 ref: any
29) {
30 const Component = as || 'span';
31 return (
32 <Component ref={ref} className={`text-${variant}`} style={{ color }} {...restProps}>
33 {children}
34 </Component>
35 );
36});
37
38// 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 Title
45 </Text>
46
47 {/* VALID: <a> tag with href and target */}
48 <Text as="a" href="https://example.com" target="_blank">
49 Click Link
50 </Text>
51
52 {/* 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.ElementType captures 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:

typescript
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
What is React.ElementType in TypeScript?

A type representing anything that can be rendered as a React component: either a string representing an HTML tag (like "div" or "a") or a React component function/class.

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.

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.

It provides a unified, consistent design system API for typography, buttons, and layout containers, reducing code duplication while enforcing semantic HTML.

Yes. Because ElementType includes React component types, you can pass as={Link} and TypeScript will infer Link's props (such as href and prefetch).

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.