React多态组件中forwardRef的TypeScript类型定义问题
问题背景
我正在开发一个基于forwardRef的多态React组件,类型配置始终存在问题。参考一篇博客的代码后解决了大部分问题,但仍有一个类型推断的疑点未理清。
博客提供的核心类型定义及组件实现代码如下:
import React from 'react' // Source: https://github.com/emotion-js/emotion/blob/master/packages/styled-base/types/helper.d.ts // A more precise version of just React.ComponentPropsWithoutRef on its own export type PropsOf< C extends keyof JSX.IntrinsicElements | React.JSXElementConstructor<any> > = JSX.LibraryManagedAttributes<C, React.ComponentPropsWithoutRef<C>> type AsProp<C extends React.ElementType> = { /** * An override of the default HTML tag. * Can also be another React component. */ as?: C } /** * Allows for extending a set of props (`ExtendedProps`) by an overriding set of props * (`OverrideProps`), ensuring that any duplicates are overridden by the overriding * set of props. */ export type ExtendableProps< ExtendedProps = {}, OverrideProps = {} > = OverrideProps & Omit<ExtendedProps, keyof OverrideProps> /** * Allows for inheriting the props from the specified element type so that * props like children, className & style work, as well as element-specific * attributes like aria roles. The component (`C`) must be passed in. */ export type InheritableElementProps< C extends React.ElementType, Props = {} > = ExtendableProps<PropsOf<C>, Props> /** * A more sophisticated version of `InheritableElementProps` where * the passed in `as` prop will determine which props can be included */ export type PolymorphicComponentProps< C extends React.ElementType, Props = {} > = InheritableElementProps<C, Props & AsProp<C>> /** * Utility type to extract the `ref` prop from a polymorphic component */ export type PolymorphicRef< C extends React.ElementType > = React.ComponentPropsWithRef<C>['ref'] /** * A wrapper of `PolymorphicComponentProps` that also includes the `ref` * prop for the polymorphic component */ export type PolymorphicComponentPropsWithRef< C extends React.ElementType, Props = {} > = PolymorphicComponentProps<C, Props> & { ref?: PolymorphicRef<C> } // 👇🏾👇🏾 sample usage in `Text` component 👇🏾👇🏾 interface Props { children: React.ReactNode color?: string; font?: 'thin' | 'regular' | 'heavy' size?: '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | '10' } type TextProps<C extends React.ElementType> = PolymorphicComponentPropsWithRef< C, Props > type TextComponent = <C extends React.ElementType = 'span'>( props: TextProps<C>, ) => React.ReactElement | null export const Text: TextComponent = React.forwardRef( <C extends React.ElementType = 'span'>( { as, children, font = 'regular', size = '4', color = 'blue', ...other }: TextProps<C>, ref?: PolymorphicRef<C>, ) => { const classes = `${color} ${font} ${size}` const Component = as || 'span' return ( <Component {...other} className={classes} ref={ref}> {children} </Component> ) }, )
具体问题
给Text组件添加onClick事件时,参数e会被隐式推断为any类型:
<Text onClick={(e) => console.log(e)}>Hello</Text>
但奇怪的是,如果组件存在其他类型错误(比如传入非法的size值),e会被正确推断为React.MouseEvent<HTMLSpanElement>类型:
<Text size="50" onClick={(e) => console.log(e)}> Hello </Text>
我有两个疑问:
- 是否可以调整类型配置,让正常无错误场景下
e也能被正确推断? - 为什么存在其他类型错误时
e的类型能被正确推断?
解答
问题1:修复正常场景下的类型推断
问题根源在于PolymorphicComponentProps的类型结构,当没有显式指定as或触发其他错误时,TypeScript无法正确缩小默认泛型'span'的范围,导致事件参数类型丢失。可以通过调整类型定义解决:
修改PolymorphicComponentProps的结构,重新组织props的交集顺序,让TypeScript优先处理默认元素的Props推导:
// 调整PolymorphicComponentProps的定义 export type PolymorphicComponentProps< C extends React.ElementType, Props = {} > = Props & AsProp<C> & Omit<PropsOf<C>, keyof (Props & AsProp<C>)> // 同步调整PolymorphicComponentPropsWithRef export type PolymorphicComponentPropsWithRef< C extends React.ElementType, Props = {} > = PolymorphicComponentProps<C, Props> & { ref?: PolymorphicRef<C> }
这种方式让TypeScript优先解析默认元素'span'的原生props,从而正确推断出事件参数的类型。
问题2:错误场景下类型推断正常的原因
当存在其他类型错误(比如size="50")时,TypeScript会先尝试用默认的'span'泛型校验所有props:此时onClick的类型会被正确解析为React.MouseEvent<HTMLSpanElement>,之后再抛出size参数的类型错误。
简单来说,当有明确的类型错误触发时,TypeScript不会放弃对合法props的类型推导,而是先完成默认泛型的类型解析,再报告错误,因此e的类型能被正确推断。而无错误时,原类型结构的交集优先级导致TypeScript无法确定C的具体类型(即使默认是span),最终事件参数被推断为any。
内容的提问来源于stack exchange,提问作者David Reid

