如何实现基于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} />; } }
关键改动说明:
- 函数重载:通过两个重载签名,明确告诉TypeScript当
as='input'时接收InputProps,as='button'时接收ButtonProps,从根源上保证类型强约束。 - 剥离
as属性:用对象解构分离as,避免传递给原生元素多余属性,消除原生元素不识别as的潜在问题。 - 分支类型收窄:实现中TypeScript能通过
as === 'input'精准收窄rest的类型,匹配对应原生元素属性,不再出现ref等属性的类型冲突。
这样修改后,组件就能满足强类型要求:比如传入as="input"时,type="submit"会直接触发编译错误;as="button"时则可以正常使用submit类型,完全符合需求。
内容的提问来源于stack exchange,提问作者rony l
相关产品推荐
相关产品推荐

