React TypeScript泛型forwardRef组件Wrapper的类型问题求解
泛型forwardRef组件类型不匹配问题修复(TypeScript 5.0.4)
问题背景
需要实现一个泛型React组件Wrapper,泛型参数T extends Kind(Kind是包含btn和div的枚举),根据props.kind渲染<Btn />或<Div />,且props和ref类型要与T关联。原代码使用forwardRef包裹后出现两个问题:
- 类型检查强制要求
kind为Kind.btn & Kind.div; - 传入不匹配的props和ref时(比如给
kind={Kind.div}传HTMLButtonElement的ref),TypeScript不报错。
原代码如下:
import { forwardRef, HTMLAttributes, ComponentProps, ForwardedRef } from 'react'; const Btn = forwardRef<HTMLButtonElement, HTMLAttributes<HTMLButtonElement>>( (props, ref) => <button {...props} ref={ref} /> ); const Div = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>( (props, ref) => <div {...props} ref={ref} /> ); enum Kind { btn, div, } type WrapperPropsType<T extends Kind> = & { kind: T } & (T extends Kind.btn ? ComponentProps<typeof Btn> : ComponentProps<typeof Div>); type WrapperArgs<T extends Kind> = { [Kind.btn]: [props: WrapperPropsType<Kind.btn>, ref: ForwardedRef<HTMLButtonElement>]; [Kind.div]: [props: WrapperPropsType<Kind.div>, ref: ForwardedRef<HTMLDivElement>]; }[T]; const isBtnPropRefPair = (args: WrapperArgs<Kind>): args is WrapperArgs<Kind.btn> => args[0].kind === Kind.btn; const _Wrapper = <T extends Kind>(...args: WrapperArgs<T>) => { if (isBtnPropRefPair(args)) { const {kind, ...props} = args[0]; return <Btn {...props} ref={args[1]} /> } else { const {kind, ...props} = args[0]; return <Div {...props} ref={args[1]} /> } }; export const Wrapper = forwardRef(_Wrapper);
测试代码中错误用法未被检测到:
const Foo = () => { const ref = useRef<HTMLButtonElement>(null); // @ts-expect-error 此处应该报错,但TypeScript无提示 return <Wrapper ref={ref} kind={Kind.div} />; };
解决方案(TypeScript 5.0.4可行)
问题出在forwardRef对泛型函数的类型推断上,需要显式指定forwardRef的泛型参数,同时调整类型定义,让ref类型与kind严格绑定。
修复后的代码:
import { forwardRef, HTMLAttributes, ComponentProps, ForwardedRef, useRef } from 'react'; const Btn = forwardRef<HTMLButtonElement, HTMLAttributes<HTMLButtonElement>>( (props, ref) => <button {...props} ref={ref} /> ); const Div = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>( (props, ref) => <div {...props} ref={ref} /> ); enum Kind { btn, div, } // 定义每种kind对应的ref类型和props类型映射 type KindMapping = { [Kind.btn]: { Ref: HTMLButtonElement; Props: ComponentProps<typeof Btn>; }; [Kind.div]: { Ref: HTMLDivElement; Props: ComponentProps<typeof Div>; }; }; // 重新定义Wrapper的props:自动关联kind对应的props和ref类型 type WrapperProps<T extends Kind> = KindMapping[T]['Props'] & { kind: T }; // 显式指定forwardRef的泛型:Ref类型由T决定,Props是WrapperProps<T> const Wrapper = forwardRef(<T extends Kind>( props: WrapperProps<T>, ref: ForwardedRef<KindMapping[T]['Ref']> ) => { const { kind, ...restProps } = props; switch (kind) { case Kind.btn: return <Btn {...restProps} ref={ref as ForwardedRef<HTMLButtonElement>} />; case Kind.div: return <Div {...restProps} ref={ref as ForwardedRef<HTMLDivElement>} />; default: throw new Error(`Unknown kind: ${kind}`); } }) as <T extends Kind>( props: WrapperProps<T> & { ref?: ForwardedRef<KindMapping[T]['Ref']> } ) => JSX.Element;
关键修改点
- 新增
KindMapping类型:统一管理每种Kind对应的ref和props类型,避免条件类型的嵌套推断问题; - 显式指定
forwardRef泛型:让TypeScript明确知道ref类型与泛型T的关联关系; - 断言
ref类型:在switch分支中,TypeScript已经能通过kind推断出T的具体值,此时断言ref类型是安全的; - 组件类型断言:最后将
forwardRef返回的组件断言为泛型组件类型,确保使用时TypeScript能正确推断props和ref的匹配关系。
现在测试代码中的错误用法会被TypeScript正确检测到:
const Foo = () => { const ref = useRef<HTMLButtonElement>(null); // ✅ TypeScript会报错:类型'MutableRefObject<HTMLButtonElement | null>'不能赋值给类型'ForwardedRef<HTMLDivElement> | undefined' return <Wrapper ref={ref} kind={Kind.div} />; };
内容的提问来源于stack exchange,提问作者dx_over_dt
相关产品推荐
相关产品推荐

