如何用TypeScript创建React多态按钮组件?
我需要实现一个多态Button组件,它可以作为原生button、锚点(a标签)或者路由链接组件使用,示例用法如下:
<Button onClick={e => console.log("click", e)}>A button</Button> <Button as="a" href="https://somewhere-external.com" rel="noopener noreferrer" target="_blank"> An anchor </Button> <Button as={Link} to="/some_page"> A Link </Button>
我看过不少关于这类组件的实现文章,但觉得现有方案太复杂,尤其是支持forwardRef的部分。我想要一套简单易用、容易理解的实现方案。
补充要求
- 用于组件库开发,不能依赖
Link这类第三方路由组件 - 需要支持headless-ui的
<Popover.Button>这类自定义组件
我自己构思了一套方案,但存在明显问题:所有事件处理程序的类型都基于HTMLButtonElement,无法适配不同元素/组件的类型:
类型文件代码
/* Types file */ export type PolymorphicProps< OwnProps, As extends ElementType, DefaultElement extends ElementType > = OwnProps & ( | (Omit<ComponentProps<As>, "as"> & { as?: As }) | (Omit<ComponentProps<As>, "as"> & { as: As }) | (Omit<ComponentProps<DefaultElement>, "as"> & { as?: never }) )
组件文件代码
/* Component file */ const defaultElement = "button" type OwnProps = {} type Props<As extends ElementType = typeof defaultElement> = PolymorphicProps< OwnProps, As, typeof defaultElement > const Button = <As extends ElementType = typeof defaultElement>( { as, children, ...attrs }: Props<As>, ref: ForwardedRef<ComponentProps<As>> ) => { const Component = as || defaultElement return ( <Component ref={ref} {...attrs}> {children} </Component> ) } export default forwardRef(Button) as typeof Button
优化后的简单实现方案
我们可以简化泛型逻辑,利用React内置的ComponentPropsWithRef自动推导不同as元素/组件的属性和ref类型,同时保持类型安全:
类型定义简化
import { ComponentPropsWithRef, ElementType, ForwardedRef, forwardRef } from "react"; // 自定义组件自有属性(可按需扩展,比如size、variant等) type ButtonOwnProps = {}; // 多态Props:自有属性 + 指定元素/组件的属性(排除as) + as字段 type PolymorphicButtonProps<As extends ElementType = "button"> = ButtonOwnProps & Omit<ComponentPropsWithRef<As>, "as"> & { as?: As; };
组件实现
const Button = <As extends ElementType = "button">( { as: Component = "button", children, ...props }: PolymorphicButtonProps<As>, ref: ForwardedRef<ComponentPropsWithRef<As>["ref"]> ) => { return <Component ref={ref} {...props}>{children}</Component>; }; export default forwardRef(Button) as <As extends ElementType = "button">( props: PolymorphicButtonProps<As>, ref: ForwardedRef<ComponentPropsWithRef<As>["ref"]> ) => ReturnType<typeof Component>;
方案说明
- 类型自动适配:通过
ComponentPropsWithRef自动获取as指定元素/组件的所有属性和ref类型,事件处理程序的类型会随as变化(比如as="a"时,onClick事件类型为MouseEvent<HTMLAnchorElement>) - 无外部依赖:完全基于React内置类型,兼容任意合法的
ElementType,包括路由组件、headless-ui组件等 - 正确支持forwardRef:ref类型会自动匹配
as指定的元素/组件,不会出现类型不兼容问题 - 逻辑简洁:去掉冗余的联合类型分支,代码结构清晰易维护
验证用法示例
// 原生button:事件类型自动适配HTMLButtonElement <Button onClick={(e) => console.log(e.currentTarget)}>Button</Button> // 锚点a标签:自动支持href、target等属性,事件类型适配HTMLAnchorElement <Button as="a" href="/" onClick={(e) => console.log(e.currentTarget)}>Anchor</Button> // 路由Link组件:自动继承Link的to等属性,ref类型匹配Link组件 <Button as={Link} to="/home">Router Link</Button> // headless-ui的Popover.Button:自动兼容组件的所有属性和类型 <Button as={Popover.Button}>Popover Trigger</Button>
内容的提问来源于stack exchange,提问作者vcarel
相关产品推荐
相关产品推荐

