For example, image an API like the the following that will render a span instead of a button.
<Button as="span">I am a span</Button>
But how does this magic work? There are some articles on popular blog posts such as robinwieruch and Developer Way, but I think most miss the mark on how to implement this in a type-safe way.
Let's dive deeper into how we can implement this ourselves (with TypeScript).
Prerequisites
Working knowledge of React.
Working knowledge of TypeScript.
Setting up the Vite project
We will use Vite to spin up a quick project to work with.
In the terminal, run the following:
pnpm create vite@latest blog-react-ts-as-prop --template react-ts
cd blog-react-ts-as-prop
pnpm install
pnpm dev
At this point, you're app should be up and running (mine was at http://localhost:5173/), so we are ready to move forward!
Understanding React.ElementType
As of writing, the definition for this type comes from here.
type ElementType<
P = any,
Tag extends keyof JSX.IntrinsicElements = keyof JSX.IntrinsicElements,
> =
| { [K in Tag]: P extends JSX.IntrinsicElements[K] ? K : never }[Tag]
| ComponentType<P>;
The tl;dr on this section is that ElementType is a type that can be either a ComponentType or a keyof JSX.IntrinsicElements where the keyof JSX.InstrinsicElements can be any valid HTML attribute key e.g. p or button.
This is the magic ingredient for an as prop, so let's implement that now.
Implementing the as prop
Within src/App.tsx, let's create a Button component that will take an as prop near the top of the file.
Great! We have a working as prop. But what about TypeScript? Right now, we can tell our button what component to render, but we cannot actually infer the props so that TypeScript can help at the call site.
Making our types a little more flexible
At this point, we need to dive into generics to help us out.
Side note: I have another post that also works through generics more in-depth.
Our problem is this: we want to be able to pass in a component and have TypeScript infer the props for us.
We can solve this problem by abstracting the button type, introducing React.ComponentPropsWithoutRef and making it generic.
Let's update our Button component to the following:
type ButtonProps<T extends React.ElementType> = T extends React.ElementType
? { as: T } & React.ComponentPropsWithoutRef<T>
: never;
function Button<T extends React.ElementType>({ as, ...props }: ButtonProps<T>) {
const Component = as;
return <Component {...props} />;
}
In the above, we have introduced a generic T that extends React.ElementType. We then use this generic to create a ButtonProps type that will take in a T and return a type that is either the as prop or never.
We then use React.ComponentPropsWithoutRef to infer the props for the T type and then use the as prop to render the component.
Let's test this out by creating a ValidDivComponent that takes in a name prop and then render it with our Button component.
function ValidDivComponent({ name }: { name: string }) {
return <div>{name}</div>;
}
Update the entire file code now to the following:
import React from "react";
import "./App.css";
type ButtonProps<T extends React.ElementType> = T extends React.ElementType
? { as: T } & React.ComponentPropsWithoutRef<T>
: never;
function Button<T extends React.ElementType>({ as, ...props }: ButtonProps<T>) {
const Component = as;
return <Component {...props} />;
}
function ValidDivComponent({ name }: { name: string }) {
return <div>{name}</div>;
}
function App() {
return (
<>
<Button as="p">p tag</Button>
<Button as="button" href="#">
actual button
</Button>
<Button as="a" href="#">
anchor tag
</Button>
<Button as={ValidDivComponent} />
</>
);
}
export default App;
Once you save, you'll notice that we now have two TypeScript errors (this is assuming your IDE is set up to use TypeScript).
Our <Button as="button" href="/"> renders an error because href is not valid.
Our <Button as={ValidDivComponent} /> renders an error because we are missing the name prop.
As an added bonus, you'll notice that our <Button as="a" href="/">anchor tag</Button> component does not render an error as href is a valid prop for an a tag.
Hooray for generics! Now we have a type-safe as prop that we can use to render any component we want.
If we update the App function code, we will be back in business with no errors: