如何用TypeScript正确实现根据isDiv属性渲染HTML元素并透传属性的React组件?
问题根因
你的实现出现类型报错,本质是两个设计上的问题导致的:
- 入参结构和使用习惯脱节:原JS版本将元素透传属性封装为单独的
elementProps对象入参,但你给出的使用示例是直接将HTML属性作为组件顶层props传入(比如<Button type="submit" />),两种写法完全不匹配,实际使用时需要手动套一层属性对象,体验很差。 - 判别联合的类型关联被解构破坏:原TS代码中
isDiv和elementProps是props上两个独立的显式定义属性,当从联合类型的props中把这两个属性解构为独立变量时,TS会分别取两个属性的联合类型,无法通过isDiv的取值反向关联elementProps对应的具体类型。最终在往div或button上传递属性时,TS会认为传入的属性集合可能是另一个元素的属性类型,而两种元素的属性集合并不完全兼容,就抛出了Property 'align' is missing in type 'HTMLButtonElement' but required in type 'HTMLDivElement'的错误。
正确实现方案
我们只需要调整类型定义结构,让判别属性isDiv和对应元素的原生属性直接绑定为联合分支,不再单独设置elementProps入参,通过rest参数收集所有需要透传的属性,就能让TS的类型收窄正常工作。
1. 类型定义
import React from 'react'; // 渲染div的分支:必须显式传入isDiv={true} type DivVariantProps = { isDiv: true; } & React.HTMLAttributes<HTMLDivElement>; // 渲染button的分支:isDiv可以不传,或显式传入isDiv={false} type ButtonVariantProps = { isDiv?: false; } & React.ButtonHTMLAttributes<HTMLButtonElement>; // 组件整体的props类型,为两个分支的判别联合 type ButtonProps = DivVariantProps | ButtonVariantProps;
这里特意将DivVariantProps的
isDiv设为必填的true,ButtonVariantProps的isDiv设为可选的false,避免undefined值带来的类型歧义,让TS能准确识别分支:
- 未传
isDiv或传isDiv={false}时,走button渲染分支- 显式传
isDiv={true}时,走div渲染分支
2. 组件实现
const Button: React.FC<ButtonProps> = (props) => { // 单独取出判别属性isDiv和children,剩余属性全部收集为透传的restProps const { isDiv, children, ...restProps } = props; if (isDiv) { // 传入div时,TS会自动收窄restProps为合法的div属性类型 // 合并类名,避免透传的className覆盖默认的button基础类 return ( <div {...restProps} className={`button ${restProps.className ?? ''}`.trim()} > {children} </div> ) } // 传入button时,TS会自动收窄restProps为合法的button属性类型 return ( <button {...restProps} className={`button ${restProps.className ?? ''}`.trim()} > {children} </button> ) }
效果验证
实现完成后完全匹配预期的使用方式,同时TS会自动做属性合法性校验:
- 不传
isDiv时,渲染button元素,支持传入所有button原生属性:<Button type="submit" onClick={() => console.log('click')} /> // 最终渲染:<button class="button" type="submit">...</button> - 传入
isDiv时,渲染div元素,支持传入所有div原生属性,传入button专属的无效属性时TS会直接报错:<Button isDiv role="button" onClick={() => console.log('click')} /> // 最终渲染:<div class="button" role="button">...</div>
内容的提问来源于stack exchange,提问作者George
相关产品推荐
相关产品推荐

