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

React Button组件TypeScript类型适配:支持button/anchor双标签

解决React Button组件同时支持button与anchor标签的TypeScript类型问题

需求是实现一个React Button组件,既能作为带href属性的<a>标签,也能作为带type属性的<button>标签。原实现代码如下:

import type { AnchorHTMLAttributes, ButtonHTMLAttributes } from 'react';

interface IBaseProps {
  children: string;
  fullSize?: boolean;
  theme?: 'primary' | 'secondary' | 'dark';
}

interface ILinkButtonProps extends IBaseProps {
  url: string;
  type: never;
  props?: AnchorHTMLAttributes<HTMLAnchorElement>;
}

interface IButtonProps extends IBaseProps {
  type: 'button' | 'submit' | 'reset';
  url: never;
  props?: ButtonHTMLAttributes<HTMLButtonElement>;
}

export const Button = ({
  children,
  props,
  theme = 'primary',
  fullSize = false,
  type = 'button',
  url,
}: IButtonProps | ILinkButtonProps): JSX.Element => {
  const Tag: keyof JSX.IntrinsicElements = url ? 'button' : 'a';

  return (
    <Tag
      className={`${styles.button} ${styles[theme]} ${
        fullSize ? styles.fullSize : ''
      }`}
      
      {...props}
      {...(Tag === "button" ? {type: `${type}`} : {href: url})}
    >
      {children}
    </Tag>
  );
};

运行时出现类型错误,示例错误信息:

Types of property 'onCopy' are incompatible.
Type 'ClipboardEventHandler | undefined' is not assignable to type 'ClipboardEventHandler | undefined'.
Type 'ClipboardEventHandler' is not assignable to type 'ClipboardEventHandler'.
Type 'HTMLAnchorElement' is missing the following properties from type 'HTMLButtonElement': disabled, form, formAction, formEnctype, and 11 more.


问题根源

原代码使用联合类型IButtonProps | ILinkButtonProps作为组件参数,但TypeScript无法在渲染时将动态的Tag类型与对应的props类型建立关联,导致AnchorHTMLAttributes和ButtonHTMLAttributes的属性被混在一起做类型检查,出现不兼容报错。


解决方案:拆分渲染逻辑+类型守卫

通过类型守卫明确区分两种props类型,分别渲染<button>和<a>标签,让TypeScript能正确推断对应标签的属性类型:

import type { AnchorHTMLAttributes, ButtonHTMLAttributes } from 'react';
import styles from './Button.module.css'; // 假设样式文件路径

interface IBaseProps {
  children: string;
  fullSize?: boolean;
  theme?: 'primary' | 'secondary' | 'dark';
}

interface ILinkButtonProps extends IBaseProps {
  url: string;
  type?: never;
  props?: AnchorHTMLAttributes<HTMLAnchorElement>;
}

interface IButtonProps extends IBaseProps {
  type: 'button' | 'submit' | 'reset';
  url?: never;
  props?: ButtonHTMLAttributes<HTMLButtonElement>;
}

type ButtonProps = IButtonProps | ILinkButtonProps;

export const Button = ({
  children,
  props,
  theme = 'primary',
  fullSize = false,
  type = 'button',
  url,
}: ButtonProps) => {
  // 类型守卫:判断是否为链接按钮
  const isLinkButton = (input: Pick<ButtonProps, 'url' | 'type'>): input is Pick<ILinkButtonProps, 'url' | 'type'> => {
    return typeof input.url !== 'undefined';
  };

  // 渲染链接按钮
  if (isLinkButton({ url, type })) {
    return (
      <a
        className={`${styles.button} ${styles[theme]} ${fullSize ? styles.fullSize : ''}`}
        href={url}
        {...props}
      >
        {children}
      </a>
    );
  }

  // 渲染普通按钮
  return (
    <button
      className={`${styles.button} ${styles[theme]} ${fullSize ? styles.fullSize : ''}`}
      type={type}
      {...props}
    >
      {children}
    </button>
  );
};

关键调整点:

  1. 类型守卫:新增isLinkButton函数,帮助TypeScript明确区分ILinkButtonProps和IButtonProps类型,消除联合类型的歧义。
  2. 拆分渲染分支:分别处理<a>和<button>的渲染逻辑,此时对应的props会被正确推断为对应元素的属性类型,解决属性不兼容问题。
  3. 严格类型约束:将type和url的类型改为?: never,避免用户同时传入这两个属性,强化类型检查。

可选方案:泛型组件+动态标签

通过泛型关联标签类型与属性类型,支持通过as属性指定渲染标签,灵活性更高:

import type { AnchorHTMLAttributes, ButtonHTMLAttributes, ElementType } from 'react';
import styles from './Button.module.css';

interface IBaseButtonProps {
  children: string;
  fullSize?: boolean;
  theme?: 'primary' | 'secondary' | 'dark';
}

// 泛型类型:根据指定的标签类型生成对应属性
type ButtonProps<T extends ElementType> = 
  T extends 'a' 
    ? IBaseButtonProps & AnchorHTMLAttributes<HTMLAnchorElement> & { url: string }
    : T extends 'button'
      ? IBaseButtonProps & ButtonHTMLAttributes<HTMLButtonElement> & { type?: 'button' | 'submit' | 'reset' }
      : never;

export const Button = <T extends ElementType = 'button'>({
  children,
  theme = 'primary',
  fullSize = false,
  as: Tag = 'button' as T,
  url,
  type = 'button',
  ...props
}: ButtonProps<T>) => {
  const baseClassName = `${styles.button} ${styles[theme]} ${fullSize ? styles.fullSize : ''}`;

  if (Tag === 'a') {
    return <a className={baseClassName} href={url} {...props as AnchorHTMLAttributes<HTMLAnchorElement>}>{children}</a>;
  }

  return <button className={baseClassName} type={type} {...props as ButtonHTMLAttributes<HTMLButtonElement>}>{children}</button>;
};

使用示例:

// 普通按钮
<Button type="submit">提交</Button>
// 链接按钮
<Button as="a" url="/home">返回首页</Button>

内容的提问来源于stack exchange,提问作者Sku

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.19 21:10:48