You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用TypeScript创建React多态按钮组件?

多态Button组件实现优化

我需要实现一个多态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>;

方案说明

  1. 类型自动适配:通过ComponentPropsWithRef自动获取as指定元素/组件的所有属性和ref类型,事件处理程序的类型会随as变化(比如as="a"时,onClick事件类型为MouseEvent<HTMLAnchorElement>)
  2. 无外部依赖:完全基于React内置类型,兼容任意合法的ElementType,包括路由组件、headless-ui组件等
  3. 正确支持forwardRef:ref类型会自动匹配as指定的元素/组件,不会出现类型不兼容问题
  4. 逻辑简洁:去掉冗余的联合类型分支,代码结构清晰易维护

验证用法示例

// 原生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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.10 12:50:25