You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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包裹后出现两个问题:

  1. 类型检查强制要求kind为Kind.btn & Kind.div;
  2. 传入不匹配的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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.21 13:52:50