I am building a library of components and I need some of them to have a customizable tag name. For example, sometimes what looks like a <button>
is actually a <a>
. So I would like to be able to use the button component like so:
<Button onClick={onClick}>Click me!</Button>
<Button as="a" href="/some-url">Click me!</Button>
Ideally, I would like the available props to be inferred based on the "as" prop:
// Throws an error because the default value of "as" is "button",
// which doesn't accept the "href" attribute.
<Button href="/some-url">Click me!<Button>
We might need to pass a custom component as well:
// Doesn't throw an error because RouterLink has a "to" prop
<Button as={RouterLink} to="/">Click me!</Button>
Here's the implementation, without TypeScript:
function Button({ as = "button", children, ...props }) {
return React.createElement(as, props, children);
}
So, how can I implement a "as" prop with TypeScript while passing down the props?
Note: I am basically trying to do what styled-components
does. But we are using CSS modules and SCSS so I can't afford adding styled-components. I am open to simpler alternatives, though.
I recently came across Iskander Samatov's article React polymorphic components with TypeScript in which they share a more complete and simpler solution:
import * as React from "react";
interface ButtonProps<T extends React.ElementType> {
as?: T;
children?: React.ReactNode;
}
function Button<T extends React.ElementType = "button">({
as,
...props
}:
ButtonProps<T>
& Omit<React.ComponentPropsWithoutRef<T>, keyof ButtonProps<T>>
) {
const Component = as || "button";
return <Component {...props} />;
}
Update 4 Apr 2024: I stumbled upon a new article: React "as" Prop Using TypeScript.
I spent some time digging into styled-components' types declarations. I was able to extract the minimum required code, here it is:
import * as React from "react";
import { Link } from "react-router-dom";
type CustomComponentProps<
C extends keyof JSX.IntrinsicElements | React.ComponentType<any>,
O extends object
> = React.ComponentPropsWithRef<
C extends keyof JSX.IntrinsicElements | React.ComponentType<any> ? C : never
> &
O & { as?: C };
interface CustomComponent<
C extends keyof JSX.IntrinsicElements | React.ComponentType<any>,
O extends object
> {
<AsC extends keyof JSX.IntrinsicElements | React.ComponentType<any> = C>(
props: CustomComponentProps<AsC, O>
): React.ReactElement<CustomComponentProps<AsC, O>>;
}
const Button: CustomComponent<"button", { variant: "primary" }> = (props) => (
<button {...props} />
);
<Button variant="primary">Test</Button>;
<Button variant="primary" to="/test">
Test
</Button>;
<Button variant="primary" as={Link} to="/test">
Test
</Button>;
<Button variant="primary" as={Link}>
Test
</Button>;
I removed a lot of stuff from styled-components which is way more complex than that. For example, they have some workaround to deal with class components which I removed. So this snippet might need to be customized for advanced use cases.