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

如何实现基于as属性的强类型React TS组件(button/input切换)

类型安全的React多元素组件实现问题

我想实现一个React组件,通过as属性(类型为'button' | 'input')决定渲染<button>还是<input>元素。组件示例用法如下:

<div>
    <InputOrButton as="input" type="color"></InputOrButton>
    <InputOrButton as="button" type="submit"></InputOrButton>
</div>

核心要求是强类型特性:比如当props.as==='input'时,传入type="submit"无法通过编译。具体来说,组件的其余属性类型要依赖as的值:

  • 当as为input时,仅允许传入React内置的input属性(即DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>)
  • 当as为button时,仅允许传入React内置的button属性(即DetailedHTMLProps<ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>)

需要一个完全类型安全的解决方案,不能用as类型断言。

我的尝试代码:

import { ButtonHTMLAttributes, DetailedHTMLProps, InputHTMLAttributes } from 'react'

type TagName = 'button' | 'input'
type Detail<T extends TagName> = T extends 'button'
  ? DetailedHTMLProps<ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>
  : DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>

type Props<T extends TagName> = {
  as: T
} & Detail<T>

export function InputOrButton<T extends 'button' | 'input'>(props: Props<T>) {
  if (props.as === 'input') {
    return <input {...props} />
  } else {
    return <button {...props} />
  }
}

但遇到了以下错误:

<input>元素上的错误

Type '{ as: T; ref?: LegacyRef<HTMLButtonElement> | undefined; key?: Key | null | undefined; autoFocus?: boolean | undefined; disabled?: boolean | undefined; ... 262 more ...; onTransitionEndCapture?: TransitionEventHandler<...> | undefined; } | { ...; }' is not assignable to type 'DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>'.
  Type '{ as: T; ref?: LegacyRef<HTMLButtonElement> | undefined; key?: Key | null | undefined; autoFocus?: boolean | undefined; disabled?: boolean | undefined; ... 262 more ...; onTransitionEndCapture?: TransitionEventHandler<...> | undefined; }' is not assignable to type 'DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>'.
    Type '{ as: T; ref?: LegacyRef<HTMLButtonElement> | undefined; key?: Key | null | undefined; autoFocus?: boolean | undefined; disabled?: boolean | undefined; ... 262 more ...; onTransitionEndCapture?: TransitionEventHandler<...> | undefined; }' is not assignable to type 'ClassAttributes<HTMLInputElement>'.
      Types of property 'ref' are incompatible.
        Type 'LegacyRef<HTMLButtonElement> | undefined' is not assignable to type 'LegacyRef<HTMLInputElement> | undefined'.
          Type '(instance: HTMLButtonElement | null) => void' is not assignable to type 'LegacyRef<HTMLInputElement> | undefined'.
            Type '(instance: HTMLButtonElement | null) => void' is not assignable to type '(instance: HTMLInputElement | null) => void'.
              Types of parameters 'instance' and 'instance' are incompatible.
                Type 'HTMLInputElement | null' is not assignable to type 'HTMLButtonElement | null'.
                  Type 'HTMLInputElement' is not assignable to type 'HTMLButtonElement' with 'exactOptionalPropertyTypes: true'. Consider adding 'undefined' to the types of the target's properties.
                    Types of property 'labels' are incompatible.
                      Type 'NodeListOf<HTMLLabelElement> | null' is not assignable to type 'NodeListOf<HTMLLabelElement>'.
                        Type 'null' is not assignable to type 'NodeListOf<HTMLLabelElement>'.ts(2322)

<button>元素上的错误

Type '{ as: T; ref?: LegacyRef<HTMLButtonElement> | undefined; key?: Key | null | undefined; autoFocus?: boolean | undefined; disabled?: boolean | undefined; ... 262 more ...; onTransitionEndCapture?: TransitionEventHandler<...> | undefined; } | { ...; }' is not assignable to type 'DetailedHTMLProps<ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>'.
  Type '{ as: T; ref?: LegacyRef<HTMLInputElement> | undefined; key?: Key | null | undefined; accept?: string | undefined; alt?: string | undefined; autoComplete?: string | undefined; ... 282 more ...; onTransitionEndCapture?: TransitionEventHandler<...> | undefined; }' is not assignable to type 'DetailedHTMLProps<ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>'.
    Type '{ as: T; ref?: LegacyRef<HTMLInputElement> | undefined; key?: Key | null | undefined; accept?: string | undefined; alt?: string | undefined; autoComplete?: string | undefined; ... 282 more ...; onTransitionEndCapture?: TransitionEventHandler<...> | undefined; }' is not assignable to type 'ClassAttributes<HTMLButtonElement>'.
      Types of property 'ref' are incompatible.
        Type 'LegacyRef<HTMLInputElement> | undefined' is not assignable to type 'LegacyRef<HTMLButtonElement> | undefined'.
          Type '(instance: HTMLInputElement | null) => void' is not assignable to type 'LegacyRef<HTMLButtonElement> | undefined'.
            Type '(instance: HTMLInputElement | null) => void' is not assignable to type '(instance: HTMLButtonElement | null) => void'.
              Types of parameters 'instance' and 'instance' are incompatible.
                Type 'HTMLButtonElement | null' is not assignable to type 'HTMLInputElement | null'.
                  Type 'HTMLButtonElement' is missing the following properties from type 'HTMLInputElement': accept, align, alt, autocomplete, and 36 more.ts(2322)

请问我的代码哪里出错了?


问题原因

你的代码核心问题在于TypeScript无法在条件分支中精准收窄泛型T的类型。虽然你通过props.as === 'input'做了运行时判断,但编译器没办法把泛型T和具体分支绑定——它依然认为props是Props<'button'> | Props<'input'>的联合类型,而非当前分支对应的具体类型,这就导致传递ref等元素专属属性时出现类型不兼容的错误。

另外,你还额外传递了as属性给原生元素,而原生<input>和<button>并没有as这个属性,这也是隐性的类型问题。

解决方法

通过重载函数签名让TypeScript精准关联as属性和对应的元素属性,同时在实现中剥离as属性:

import { ButtonHTMLAttributes, DetailedHTMLProps, InputHTMLAttributes, ReactElement } from 'react';

// 定义两种场景的Props类型
type InputProps = DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement> & {
  as: 'input';
};

type ButtonProps = DetailedHTMLProps<ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement> & {
  as: 'button';
};

// 函数重载签名,明确不同as对应的参数和返回值
export function InputOrButton(props: InputProps): ReactElement;
export function InputOrButton(props: ButtonProps): ReactElement;

// 函数实现
export function InputOrButton(props: InputProps | ButtonProps): ReactElement {
  const { as, ...rest } = props;
  if (as === 'input') {
    return <input {...rest} />;
  } else {
    return <button {...rest} />;
  }
}

关键改动说明:

  1. 函数重载:通过两个重载签名,明确告诉TypeScript当as='input'时接收InputProps,as='button'时接收ButtonProps,从根源上保证类型强约束。
  2. 剥离as属性:用对象解构分离as,避免传递给原生元素多余属性,消除原生元素不识别as的潜在问题。
  3. 分支类型收窄:实现中TypeScript能通过as === 'input'精准收窄rest的类型,匹配对应原生元素属性,不再出现ref等属性的类型冲突。

这样修改后,组件就能满足强类型要求:比如传入as="input"时,type="submit"会直接触发编译错误;as="button"时则可以正常使用submit类型,完全符合需求。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.10 00:45:36