
Handling valid component attributes with TypeScript generics
Handling valid component attributes with TypeScript generics êŽë š
In solving this second use case, youâll come to appreciate how powerful generics truly are. First, letâs understand what weâre trying to accomplish here.
Once we receive a generic as type, we want to make sure that the remaining props passed to our component are relevant, based on the as prop.
So, for example, if a user passed in an as prop of img, weâd want href to equally be a valid prop!

To give you a sense of how weâd accomplish this, take a look at the current state of our solution:
export const Text = <C extends React.ElementType>({
as,
children,
}: {
as?: C;
children: React.ReactNode;
}) => {
const Component = as || "span";
return <Component>{children}</Component>;
};
The prop of this component is now represented by the object type:
{
as?: C;
children: React.ReactNode;
}
In pseudocode, what weâd like would be the following:
{
as?: C;
children: React.ReactNode;
} & {
...otherValidPropsBasedOnTheValueOfAs
}

This requirement is enough to leave one grasping at straws. We canât possibly write a function that determines appropriate types based on the value of as, and itâs not smart to list out a union type manually.
Well, what if there was a provided type from React that acted as a âfunctionâ thatâll return valid element types based on what you pass it?
Before introducing the solution, letâs have a bit of a refactor. Letâs pull out the props of the component into a separate type:
// đ See TextProps pulled out below
type TextProps<C extends React.ElementType> = {
as?: C;
children: React.ReactNode;
}
export const Text = <C extends React.ElementType>({
as,
children,
}: TextProps<C>) => { // đ see TextProps used
const Component = as || "span";
return <Component>{children}</Component>;
};
Whatâs important here is to note how the generic is passed on to TextProps<C>. Similar to a function call in JavaScript â but with angle braces.
The magic wand here is to leverage the React.ComponentPropsWithoutRef type as shown below:
type TextProps<C extends React.ElementType> = {
as?: C;
children: React.ReactNode;
} & React.ComponentPropsWithoutRef<C>; // đ look here
export const Text = <C extends React.ElementType>({
as,
children,
}: TextProps<C>) => {
const Component = as || "span";
return <Component>{children}</Component>;
};
Note that weâre introducing an intersection here. Essentially, weâre saying, the type of TextProps is an object type containing as, children, and some other types represented by React.ComponentPropsWithoutRef.

React.ComponentPropsWithoutRefIf you read the code, it perhaps becomes apparent whatâs going on here.
Based on the type of as, represented by the generic C, React.componentPropsWithoutRef will return valid component props that correlate with the string attribute passed to the as prop.
Thereâs one more significant point to note.

ComponentProps Type VariantsIf you just started typing and rely on IntelliSense from your editor, youâd realize there are three variants of the React.ComponentProps... type:
React.ComponentPropsReact.ComponentPropsWithRefReact.ComponentPropsWithoutRef
If you attempted to use the first, ComponentProps, youâd see a relevant note that reads:
Prefer
ComponentPropsWithRef, if therefis forwarded, orComponentPropsWithoutRefwhen refs are not supported.

This is precisely what weâve done. For now, we will ignore the use case for supporting a ref prop and stick to ComponentPropsWithoutRef.
Now, letâs give the solution a try!
If you go ahead and use this component wrongly, e.g., passing a valid as prop with other incompatible props, youâll get an error.
<Text as="div" href="www.google.com">Hello Text world</Text>
A value of div is perfectly valid for the as prop, but a div should not have an href attribute.
Thatâs wrong, and rightly caught by TypeScript with the error: Property 'href' does not exist on type ....

This is great! Weâve got an even better, more robust solution.
Finally, make sure to pass other props down to the rendered element:
type TextProps<C extends React.ElementType> = {
as?: C;
children: React.ReactNode;
} & React.ComponentPropsWithoutRef<C>;
export const Text = <C extends React.ElementType>({
as,
children,
...restProps, // đ look here
}: TextProps<C>) => {
const Component = as || "span";
// see restProps passed đ
return <Component {...restProps}>{children}</Component>;
};
Letâs keep going.