TypeScript中使用forwardRef与HOC的类型兼容问题排查
问题背景
开发基于MUI的React高阶组件withTouchRipple,初始代码用any作为props类型可正常运行,但希望替换为泛型PROPS以暴露原组件的props接口,修改后触发TypeScript编译报错。
初始可运行代码
import React, { type FC, forwardRef } from 'react' import { ButtonBase, ButtonBaseProps } from '@mui/material' export default function withTouchRipple<PROPS>(OriginalComponent: FC<PROPS>) { return forwardRef((props: any, ref) => <ButtonBase {...props} ref={ref} component={OriginalComponent} />) }
修改后报错的代码
import React, { type FC, forwardRef } from 'react' import { ButtonBase, ButtonBaseProps } from '@mui/material' export default function withTouchRipple<PROPS>(OriginalComponent: FC<PROPS>) { return forwardRef((props: PROPS, ref) => <ButtonBase {...props} ref={ref} component={OriginalComponent} />) }
TypeScript编译错误
No overload matches this call.
Overload 1 of 3, '(props: { href: string; } & { action?: Ref| undefined; centerRipple?: boolean | undefined; children?: ReactNode; classes?: Partial | undefined; ... 10 more ...; touchRippleRef?: Ref<...> | undefined; } & Omit<...> & CommonProps & Omit<...>): Element', gave the following error.
Type 'ForwardedRef' is not assignable to type '((instance: HTMLAnchorElement | null) => void) | RefObject | null | undefined'.
Type 'MutableRefObject' is not assignable to type '((instance: HTMLAnchorElement | null) => void) | RefObject | null | undefined'.
Type 'MutableRefObject' is not assignable to type 'RefObject '.
Types of property 'current' are incompatible.
Type 'unknown' is not assignable to type 'HTMLAnchorElement | null'.
Overload 2 of 3, '(props: { component: FC; } & { action?: Ref | undefined; centerRipple?: boolean | undefined; children?: ReactNode; ... 11 more ...; touchRippleRef?: Ref<...> | undefined; } & Omit<...> & CommonProps & DistributiveOmit<...>): Element | null', gave the following error.
Type 'PROPS & { ref: ForwardedRef; component: FC ; }' is not assignable to type 'IntrinsicAttributes & { component: FC ; } & { action?: Ref | undefined; centerRipple?: boolean | undefined; ... 12 more ...; touchRippleRef?: Ref<...> | undefined; } & Omit<...> & CommonProps & DistributiveOmit<...>'.
Type 'PROPS & { ref: ForwardedRef; component: FC ; }' is not assignable to type 'DistributiveOmit<PropsWithRef , keyof CommonProps | "tabIndex" | "children" | "action" | "centerRipple" | "disabled" | "disableRipple" | "disableTouchRipple" | ... 6 more ... | "touchRippleRef">'.
Overload 3 of 3, '(props: DefaultComponentProps<ExtendButtonBaseTypeMap<ButtonBaseTypeMap<{}, "button">>>): Element | null', gave the following error.
Type 'ForwardedRef' is not assignable to type '((instance: HTMLButtonElement | null) => void) | RefObject | null | undefined'.
Type 'MutableRefObject' is not assignable to type '((instance: HTMLButtonElement | null) => void) | RefObject | null | undefined'.
Type 'MutableRefObject' is not assignable to type 'RefObject '.
Types of property 'current' are incompatible.
Type 'unknown' is not assignable to type 'HTMLButtonElement | null'.ts(2769)
index.d.ts(805, 46): The expected type comes from property 'ref' which is declared here on type 'IntrinsicAttributes & { href: string; } & { action?: Ref| undefined; centerRipple?: boolean | undefined; children?: ReactNode; ... 11 more ...; touchRippleRef?: Ref<...> | undefined; } & Omit<...> & CommonProps & Omit<...>'
index.d.ts(805, 46): The expected type comes from property 'ref' which is declared here on type 'IntrinsicAttributes & { action?: Ref| undefined; centerRipple?: boolean | undefined; children?: ReactNode; ... 11 more ...; touchRippleRef?: Ref<...> | undefined; } & Omit<...> & CommonProps & Omit<...>'
解决方案
报错核心原因:
forwardRef未指定泛型类型,导致ref被推断为unknown,与ButtonBase期望的ref类型不匹配;- 未正确处理ButtonBase props与原组件props的类型交集,且ref传递逻辑未明确类型约束。
修正后的代码方案1(基础兼容版)
import React, { type FC, forwardRef, type PropsWithRef } from 'react' import { ButtonBase, ButtonBaseProps } from '@mui/material' export default function withTouchRipple<PROPS, REF>(OriginalComponent: FC<PropsWithRef<PROPS>>) { return forwardRef<REF, PROPS>((props, ref) => ( <ButtonBase {...props} component={OriginalComponent} ref={ref as React.Ref<unknown>} /> )) }
修正后的代码方案2(精确类型约束版)
import React, { type FC, forwardRef, type PropsWithRef } from 'react' import { ButtonBase, type ButtonBaseProps } from '@mui/material' // 排除ButtonBase自带的props,避免与原组件props冲突 type PropsWithoutButtonBase<PROPS> = Omit<PROPS, keyof ButtonBaseProps> export default function withTouchRipple<PROPS, REF>(OriginalComponent: FC<PropsWithRef<PropsWithoutButtonBase<PROPS>>>) { return forwardRef<REF, PropsWithoutButtonBase<PROPS>>((props, ref) => ( <ButtonBase component={OriginalComponent} ref={ref}> {props} </ButtonBase> )) }
关键说明
- 给
forwardRef显式指定泛型参数<REF, PROPS>,明确ref的目标类型和props类型; - 使用
PropsWithRef包裹原组件props,确保ref能正确传递; - 若原组件props与ButtonBase props存在重叠,用
Omit排除重复属性,避免类型冲突; - 类型断言
ref as React.Ref<unknown>仅在确保类型安全的场景下使用,用于兼容不同ref类型约束。
内容的提问来源于stack exchange,提问作者user21281053

