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.
Advertisement
Answer
New answer
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} />; }
Old answer
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.