React+TypeScript下如何限制ButtonGroup组件仅接受Button类型作为子组件?
我太懂你这种刚转TypeScript的困扰了——用React和PropTypes用了这么久,想给ButtonGroup加个严格限制,只让Button组件当它的子元素,结果现在只要别的组件(比如Input)的props结构和Button匹配,TypeScript就睁一只眼闭一只眼,而且用displayName做运行时检查还被TS报错,确实闹心。
咱们一步步来解决这个问题,分编译时类型校验和运行时兜底检查两部分:
第一步:给ButtonGroup的children加上组件类型限制
你之前的buttonGroupProps里,children: ReactElement<buttonProps>[]只限制了子元素的props结构,但没限制组件本身是谁。要让TypeScript识别“必须是Button组件创建的元素”,得用ReactElement的第二个泛型参数,指定组件的构造函数类型。
首先先把Button的类型明确下来(确保TS能捕获到它的构造函数类型):
import { ReactElement, ReactNode, Children, FC } from 'react' type ButtonVariantType = 'primary' | 'secondary' | 'danger' | undefined // 先定义Button的Props类型 type ButtonProps = { id?: string variant?: ButtonVariantType children: ReactNode // 其他你需要的props... } // 用FC(FunctionComponent)定义Button,让TS捕获它的类型 const Button: FC<ButtonProps> = ({ children, id, variant = 'secondary' }) => { return <button id={id} className={`btn btn-${variant}`}>{children}</button> } // 可以给Button加个displayName,方便调试,但不是必须的 Button.displayName = 'Button'
然后修改ButtonGroup的Props类型,指定children必须是Button组件创建的元素:
interface ButtonGroupProps { // 这里第二个泛型参数typeof Button,就是指定这个ReactElement必须是Button组件生成的 children: ReactElement<ButtonProps, typeof Button>[] variant?: ButtonVariantType } const ButtonGroup = ({ children, variant }: ButtonGroupProps) => { return ( <div className="button-group"> {Children.map(children, (child) => { // 现在TS知道child.type就是Button,不会再报错了 if (child.type !== Button) { console.error('ButtonGroup的子元素必须是Button组件') throw new Error('ButtonGroup仅接受Button作为子组件') } // 覆盖variant属性,如果ButtonGroup传了variant就用它,否则用Button自己的 return ( <Button {...child.props} variant={variant ?? child.props.variant}> {child.props.children} </Button> ) })} </div> ) }
第二步:验证效果
现在你再试一下把Input放进ButtonGroup里:
<ButtonGroup variant="danger"> {/* 这里TypeScript会直接报错:类型'ReactElement<InputProps, typeof Input>'不能赋值给类型'ReactElement<ButtonProps, typeof Button>' */} <Input id="123" /> <Button variant="primary" id="primary"> Primary button </Button> <Button variant="secondary" id="secondary"> Secondary button </Button> </ButtonGroup>
编译时TS就会直接拦截错误,完美达到你要的效果!
为什么原来的方式不行?
- 之前的
ReactElement<buttonProps>只检查props的结构是否匹配,不管这个元素是哪个组件生成的——只要Input的props里有id、variant这些字段(哪怕是巧合),TS就会认为它符合要求。 - 用
typeof Button作为ReactElement的第二个泛型参数,相当于告诉TS:“这个元素必须是由Button这个函数组件创建的”,从根源上限制了组件类型。
运行时检查的小补充
虽然编译时TS已经帮我们拦截了错误,但如果有人用any绕过类型检查(比如{/* @ts-ignore */}或者直接传any类型的元素),运行时的检查还是能兜底。而且现在直接比较child.type === Button,比依赖displayName更可靠——毕竟displayName是可以被修改的,而组件本身的引用不会轻易变。
备注:内容来源于stack exchange,提问作者Martin Maco

