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

如何用TypeScript更优地设置组件条件式Props?

优化多行为Button组件的TypeScript类型定义

问题背景

我正在为团队设计系统开发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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.11 10:35:36