如何用TypeScript更优地设置组件条件式Props?
问题背景
我正在为团队设计系统开发Button组件,需要支持按钮、链接等多种行为模式。要求当使用对应类型不支持的属性时,TypeScript能抛出错误。比如:
// TypeScript 应该报错,因为 action="default" 对应原生button元素,href不是它的合法属性 <Button action="default" href="/">Foo</Button>
目前我有一个可行但繁琐的实现方案,扩展性很差,新增action类型时要写大量重复代码:
type ButtonOptions = HTMLAnchorElement | HTMLButtonElement | RemixLinkProps; interface SharedProps { children: ReactNode; className?: string; size?: 'sm' | 'md' | 'lg' | 'full'; variant?: 'solid' | 'outline'; isDisabled?: boolean; isLoading?: boolean; loadingText?: string; } interface DefaultProps extends ButtonHTMLAttributes<ButtonOptions> { action?: 'default'; type?: 'submit' | 'reset' | 'button'; // 禁止这些属性 textToCopy?: never; href?: never; target?: never; to?: never; } interface AnchorProps extends AnchorHTMLAttributes<ButtonOptions> { action: 'anchor'; href: string; target?: string; // 禁止这些属性 textToCopy?: never; isDisabled?: never; isLoading?: never; loadingText?: never; to?: never; } interface LinkProps extends AnchorHTMLAttributes<ButtonOptions> { action: 'link'; to: string; // 禁止这些属性 textToCopy?: never; isDisabled?: never; isLoading?: never; loadingText?: never; target?: never; href?: never; } export type Props = SharedProps & (DefaultProps | AnchorProps | LinkProps); export const Button: FC<Props> = (props: Props) => { // 组件逻辑 };
我尝试用泛型优化,但作为TypeScript新手一直没成功,还频繁出现循环引用问题。有没有更优的实现方式?
优化方案:泛型+映射类型实现可扩展约束
核心思路是先定义每种action对应的专属属性和禁用属性,再通过泛型和条件类型组合出最终的Props类型,新增action时只需在配置对象里加一行即可,大幅提升扩展性。
完整代码示例
import type { ReactNode, ButtonHTMLAttributes, AnchorHTMLAttributes, FC } from 'react'; import type { RemixLinkProps } from '@remix-run/react'; // 定义所有action类型的配置:key是action值,value包含允许/禁止的属性 type ActionConfig = { default: { allowedProps: ButtonHTMLAttributes<HTMLButtonElement> & { action?: 'default'; type?: 'submit' | 'reset' | 'button'; }; forbiddenProps: 'textToCopy' | 'href' | 'target' | 'to'; }; anchor: { allowedProps: AnchorHTMLAttributes<HTMLAnchorElement> & { action: 'anchor'; href: string; target?: string; }; forbiddenProps: 'textToCopy' | 'isDisabled' | 'isLoading' | 'loadingText' | 'to'; }; link: { allowedProps: RemixLinkProps & { action: 'link'; to: string; }; forbiddenProps: 'textToCopy' | 'isDisabled' | 'isLoading' | 'loadingText' | 'target' | 'href'; }; }; // 提取所有action类型 type ActionType = keyof ActionConfig; // 共享属性 interface SharedProps { children: ReactNode; className?: string; size?: 'sm' | 'md' | 'lg' | 'full'; variant?: 'solid' | 'outline'; isDisabled?: boolean; isLoading?: boolean; loadingText?: string; } // 生成单个action对应的Props类型:合并允许属性,禁用指定属性 type ButtonProps<T extends ActionType> = SharedProps & ActionConfig[T]['allowedProps'] & { [K in ActionConfig[T]['forbiddenProps']]?: never }; // 最终对外暴露的Props类型,覆盖所有action分支 export type Props = { [T in ActionType]: ButtonProps<T> }[ActionType]; export const Button: FC<Props> = (props: Props) => { const { action = 'default', ...rest } = props; // 根据action渲染对应元素 if (action === 'anchor') { return <a {...rest as AnchorHTMLAttributes<HTMLAnchorElement>} />; } if (action === 'link') { return <Link {...rest as RemixLinkProps} />; } return <button {...rest as ButtonHTMLAttributes<HTMLButtonElement>} />; };
方案优势
- 高扩展性:新增action类型时,只需在
ActionConfig中添加对应的配置项,无需重复定义整个接口 - 类型安全:自动约束每个action的合法属性,传入禁用属性时TypeScript会立即报错
- 代码简洁:通过映射类型和条件类型减少重复代码,逻辑更清晰
新增action示例
如果要支持复制文本的copy类型,只需在ActionConfig中新增:
copy: { allowedProps: { action: 'copy'; textToCopy: string; }; forbiddenProps: 'href' | 'target' | 'to' | 'isDisabled' | 'isLoading'; };
内容的提问来源于stack exchange,提问作者stevenbister
相关产品推荐
相关产品推荐

