# Type-safe usage of React as a prop for flexible components

> Ever wondered how design systems provide an 'as' prop for their components that you can use to inject or change the component type? Let's look at how we can do this with TypeScript.

- Canonical: https://dennisokeeffe.com/blog/2024-01-03-typescript-react-as-prop-for-flexible-components
- Published: 2024-01-03
- Updated: 2024-01-03
- Tags: typescript, react

## Introduction

Design systems like [Chakra UI](https://chakra-ui.com/community/recipes/as-prop), [Gestalt](https://gestalt.pinterest.systems/web/box#Props-as), [Material UI](https://mui.com/material-ui/api/box/#props) (as `component`, not `as`) and [React Bootstrap](https://react-bootstrap.github.io/docs/getting-started/introduction/#as-prop-api) provide ways to override the React component in question with a prop.

For example, image an API like the the following that will render a `span` instead of a `button`.

```tsx
<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](https://www.robinwieruch.de/react-as-prop/) and [Developer Way](https://www.developerway.com/posts/react-component-as-prop-the-right-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:

```bash
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](https://github.com/DefinitelyTyped/DefinitelyTyped/blob/db81a81898e9e0eaf41c95f29e07e33fedc7a2b2/types/react/index.d.ts#L40).

```tsx
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.

```tsx
function Button({
  as,
  children,
}: {
  as: React.ElementType;
  children: React.ReactNode;
}) {
  return React.createElement(as, null, children);
}
```

Based off what we know, we should be able to pass in any valid HTML attribute key or React component and have it render as that element.

Let's test this out by rendering a `button`, a `p` element and a random component. The entire file will end up looking like this:

```tsx
import React from "react";
import "./App.css";

function Button({
  as,
  children,
}: {
  as: React.ElementType;
  children: React.ReactNode;
}) {
  return React.createElement(as, null, children);
}

function ValidDivComponent({ children }: { children: React.ReactNode }) {
  return <div>{children}</div>;
}

function App() {
  return (
    <>
      <Button as="p">p tag</Button>
      <Button as="button">actual button</Button>
      <Button as="a">anchor tag</Button>
      <Button as={ValidDivComponent}>div from component</Button>
    </>
  );
}

export default App;
```

If you were to open the browser console and check the HTML, you will see the following HTML from the `#root` div:

```html
<div id="root">
  <p>p tag</p>
  <button>actual button</button>
  <a>anchor tag</a>
  <div>div from component</div>
</div>
```

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](https://www.typescriptlang.org/docs/handbook/2/generics.html) to help us out.

> Side note: I have another [post](https://blog.dennisokeeffe.com/blog/2023-06-20-advanced-typescript-generics-in-practice) 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:

```tsx
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.

```tsx
function ValidDivComponent({ name }: { name: string }) {
  return <div>{name}</div>;
}
```

Update the entire file code now to the following:

```tsx
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).

1. Our `<Button as="button" href="/">` renders an error because `href` is not valid.
2. 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:

```tsx
function App() {
  return (
    <>
      <Button as="p">p tag</Button>
      <Button as="button">actual button</Button>
      <Button as="a" href="#">
        anchor tag
      </Button>
      <Button as={ValidDivComponent} name="div component" />
    </>
  );
}
```

But what happens if we don't want to accept certain intrinsic elements? Let's look at how we can do that next.

## Restricting the `as` prop

The TypeScript [Extract utility](https://www.typescriptlang.org/docs/handbook/utility-types.html#extracttype-union) enables us to extract common types between a type argument and a union argument.

For example:

```ts
type T0 = Extract<"a" | "b" | "c", "a" | "f">;
```

In the above, `T0` will be `"a"` as it is the only common type between the two arguments.

We can use this to restrict the `as` prop to only accept certain HTML elements.

Let's first create a new `LimitedElementType` to allow for anything that is a React component or a `button`, `a` or `span` tag.

```tsx
type LimitedElementType = Extract<
  React.ElementType,
  "button" | "a" | "span" | React.JSXElementConstructor<unknown>
>;
```

With that in place, let's update our `Button` code and props:

```tsx
type ButtonProps<T extends LimitedElementType> = T extends LimitedElementType
  ? { as: T } & React.ComponentPropsWithoutRef<T>
  : never;

function Button<T extends LimitedElementType>({
  as,
  ...props
}: ButtonProps<T>) {
  const Component = as;
  return <Component {...props} />;
}
```

At this point, you'll notice our that we have one new error: `<Button as="p">p tag</Button>` has the following error:

```text
Type '"p"' is not assignable to type '"a" | "button" | "span" | ComponentClass<any, any> | FunctionComponent<any>'.
```

This is because we have restricted the `as` prop to only accept certain types. Perfect!

The last step is to handle a default option of `button` if no `as` prop is passed in.

## Defaulting to button

In order to add this part, all we need to do is add some default values across the code.

```tsx
import React from "react";
import "./App.css";

type LimitedElementType = Extract<
  React.ElementType,
  "button" | "a" | "span" | React.JSXElementConstructor<unknown>
>;

type ButtonProps<T extends LimitedElementType = "button"> =
  T extends LimitedElementType
    ? { as?: T } & React.ComponentPropsWithoutRef<T>
    : never;

function Button<T extends LimitedElementType = "button">({
  as = "button",
  ...props
}: ButtonProps<T>) {
  const Component = as;
  return <Component {...props} />;
}

function ValidDivComponent({ name }: { name: string }) {
  return <div>{name}</div>;
}

function App() {
  return (
    <>
      <Button as="button">actual button</Button>
      <Button as="button">default button</Button>
      <Button as="a" href="/">
        anchor tag
      </Button>
      <Button as={ValidDivComponent} name="Hello" />
    </>
  );
}
export default App;
```

The first step was to add a default value of `"button"` to the `LimitedElementType` type.

```tsx
type ButtonProps<T extends LimitedElementType = "button"> =
  T extends LimitedElementType
    ? { as?: T } & React.ComponentPropsWithoutRef<T>
    : never;
```

Secondly, we added that to our `Button` component and also added the default to our `as` prop.

```tsx
function Button<T extends LimitedElementType = "button">({
  as = "button",
  ...props
}: ButtonProps<T>) {
  const Component = as;
  return <Component {...props} />;
}
```

Now you can run the code and see that our `default button` renders as a `button` element.

Even better, if you add `href` without an `as` prop, you will get an error.

Happy days!

## Conclusion

In this post, we have looked at how we can implement a type-safe `as` prop for our React components.

We have also looked at how we can restrict the `as` prop to only accept certain HTML elements.

I hope you have found this post useful and it helps you in your React TypeScript journey!

You can find the code on this [TypeScript Playground](https://tsplay.dev/mAL0kN) or

## References and Further Reading

1. [Chakra UI as prop](https://chakra-ui.com/community/recipes/as-prop)
2. [TypeScript generics](https://www.typescriptlang.org/docs/handbook/2/generics.html)
3. [TypeScript Extract utility](https://www.typescriptlang.org/docs/handbook/utility-types.html#extracttype-union)
4. [Advanced TypeScript: Generics in Practice](https://blog.dennisokeeffe.com/blog/2023-06-20-advanced-typescript-generics-in-practice)
5. [Blog code](https://github.com/okeeffed/blog-react-ts-as-prop)
6. [TypeScript Playground](https://tsplay.dev/mAL0kN)

_Photo credit: [`boliviainteligente`](https://unsplash.com/@boliviainteligente)_
