带默认渲染属性的泛型React组件为何PropTypes校验失败?
我正在尝试创建一个带泛型类型渲染属性(render-prop)的React组件,为了方便使用给这个渲染属性设置了默认值。代码功能正常,但类型检查时出现了PropTypes与TypeScript规范冲突的警告,想请教报错原因和修复方案。
错误信息
src/test.tsx:19:3 - error TS2322: Type '{ ({ data }: TChildProps
): JSX.Element; propTypes: { data: PropTypes.Validator<NonNullable<PropTypes.InferProps<{ id: PropTypes.Validator ; }>>>; }; }' is not assignable to type 'FunctionComponent<TChildProps >'.
The types of 'propTypes.data' are incompatible between these types.
Type 'Validator<NonNullable<InferProps<{ id: Validator; }>>>' is not assignable to type 'null extends V ? Validator<V | (V & null) | undefined> : undefined extends V ? Validator<V | (V & undefined) | null> : Validator '. 19 Child = DefaultChild,
可复现代码
import PropTypes from "prop-types"; export interface TData { id: string; } export interface TChildProps<V extends TData> { data: V; } export interface TParentProps<V extends TData> { data: V; Child?: React.ComponentType<TChildProps<V>>; // 引发PropTypes验证错误 } export const Parent = <V extends TData>({ data, Child = DefaultChild, }: TParentProps<V>) => { return <Child data={data}></Child>; }; export const DefaultChild = ({ data }: TChildProps<TData>) => { return <p>{data.id}</p>; }; DefaultChild.propTypes = { data: PropTypes.shape({ id: PropTypes.string.isRequired, }).isRequired, };
已尝试的临时方案
虽然不算完美,但可以通过将DefaultChild.propTypes的类型显式断言为object来屏蔽错误:
DefaultChild.propTypes = { data: PropTypes.shape({ id: PropTypes.string.isRequired, }).isRequired, } as object;
核心问题是泛型约束与PropTypes的类型范围不匹配:
Parent组件的泛型V是TData的子类型,意味着V可以是TData的任意扩展类型;- 但
DefaultChild的propTypes是基于基础TData定义的,TypeScript会判定它只能接收TData类型的data,无法兼容V这种更宽泛的子类型; - React的
ComponentType类型会同时校验组件的Props类型和propTypes的类型,最终导致两者的data约束冲突。
方案1:精准类型断言(推荐)
不用直接断言为object,而是给DefaultChild做精准的类型断言,既解决冲突又保留大部分类型校验:
export const Parent = <V extends TData>({ data, Child = DefaultChild as React.ComponentType<TChildProps<V>>, }: TParentProps<V>) => { return <Child data={data}></Child>; };
方案2:让DefaultChild支持泛型
把DefaultChild改成泛型组件,使其能兼容任意TData的子类型,让propTypes的基础约束自动匹配泛型:
export const DefaultChild = <V extends TData>({ data }: TChildProps<V>) => { return <p>{data.id}</p>; }; DefaultChild.propTypes = { data: PropTypes.shape({ id: PropTypes.string.isRequired, }).isRequired, };
方案3:移除PropTypes(纯TS项目首选)
如果你的项目已经完全用TypeScript做编译期类型校验,PropTypes属于冗余的运行时校验。直接移除DefaultChild.propTypes就能彻底解决冲突:
// 删除以下代码 // DefaultChild.propTypes = { // data: PropTypes.shape({ // id: PropTypes.string.isRequired, // }).isRequired, // };
方案4:调整Parent的泛型约束
如果业务上不需要V是TData的任意子类型,可修改Parent的泛型定义,限制V必须是TData的子类型,让DefaultChild的propTypes能匹配约束:
export const Parent = <V = TData>({ data, Child = DefaultChild, }: TParentProps<V extends TData ? V : TData>) => { return <Child data={data}></Child>; };
内容的提问来源于stack exchange,提问作者andypea

