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

React多态组件中forwardRef的TypeScript类型定义问题

多态React组件的类型推断问题

问题背景

我正在开发一个基于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>

我有两个疑问:

  1. 是否可以调整类型配置,让正常无错误场景下e也能被正确推断?
  2. 为什么存在其他类型错误时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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.19 21:48:25