React Button组件TypeScript类型适配:支持button/anchor双标签
需求是实现一个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> ); };
关键调整点:
- 类型守卫:新增
isLinkButton函数,帮助TypeScript明确区分ILinkButtonProps和IButtonProps类型,消除联合类型的歧义。 - 拆分渲染分支:分别处理
<a>和<button>的渲染逻辑,此时对应的props会被正确推断为对应元素的属性类型,解决属性不兼容问题。 - 严格类型约束:将
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

