React TypeScript多态组件限制ElementType时的类型错误问题
问题背景
我想要创建一个可限制ElementType的React多态组件,先定义了以下多态组件类型:
初始多态组件类型定义
type ComponentWithElementProps<Props, E extends React.ElementType | React.JSX.Element> = Props & Omit<React.ComponentProps<E>, keyof Props> type PolymorphicComponentProps<Props, E extends React.ElementType | React.JSX.Element> = {as?: E} & Props & ComponentWithElementProps<Props, E>
基础组件实现
type FooOwnProps = { someProp: string } type FooProps<E extends React.ElementType = 'div'> = PolymorphicComponentProps<FooOwnProps, E> function Foo<E extends React.ElementType = 'div'>({as, ...props}: FooProps<E>) { const Com = as || 'div' return ( <Com {...props}>Test</Com> ) }
这个实现运行正常,但当我限制as属性仅接受a、label这类属性差异较大的元素时,出现了TypeScript类型错误,尽管组件实际运行不受影响:
限制ElementType后的实现
type LimitedElementType = Extract<React.ElementType, 'a' | 'label'> type FooProps<E extends LimitedElementType> = PolymorphicComponentProps<FooOwnProps, E> function Foo<E extends LimitedElementType = 'a'>({as, ...props}: FooProps<E>) { const Com = as || 'a' return ( <Com {...props}>Test</Com> ) }
出现的TypeScript错误
Type 'Omit<Foo
, "as"> & { children: string; }' is not assignable to type 'IntrinsicAttributes & ((ClassAttributes & AnchorHTMLAttributes & ClassAttributes<...> & LabelHTMLAttributes<...> & ClassAttributes<...> & HTMLAttributes<...>) | LibraryManagedAttributes<...>)'.
Type 'Omit<Foo, "as"> & { children: string; }' is not assignable to type 'IntrinsicAttributes & ClassAttributes & AnchorHTMLAttributes & ClassAttributes<...> & LabelHTMLAttributes<...> & ClassAttributes<...> & HTMLAttributes<...>'.
Type 'Omit<Foo, "as"> & { children: string; }' is not assignable to type 'ClassAttributes '.
Types of property 'ref' are incompatible.
Type 'Foo["ref"] | undefined' is not assignable to type 'LegacyRef | undefined'.
Type 'ComponentProps["ref"]' is not assignable to type 'LegacyRef | undefined'.
Type 'string | ((instance: HTMLAnchorElement | null) => void) | RefObject| ((instance: HTMLLabelElement | null) => void) | RefObject<...> | null | undefined' is not assignable to type 'LegacyRef | undefined'.
Type '(instance: HTMLLabelElement | null) => void' is not assignable to type 'LegacyRef| undefined'.
Type '(instance: HTMLLabelElement | null) => void' is not assignable to type '(instance: HTMLAnchorElement | null) => void'.
Types of parameters 'instance' and 'instance' are incompatible.
Type 'HTMLAnchorElement | null' is not assignable to type 'HTMLLabelElement | null'.
Type 'HTMLAnchorElement' is missing the following properties from type 'HTMLLabelElement': control, form, htmlFor(2322)
实际运行情况
// 以下两个组件均可正常运行 <Foo as="a" href='' /> <Foo as="label" htmlFor='' />
解决方案
问题根源是TypeScript无法正确区分不同元素的ref类型,导致交叉类型冲突。以下是几种修复方案:
1. 优化多态组件类型定义,分离ref处理
使用React内置的ComponentPropsWithRef替代ComponentProps,确保ref类型与指定元素严格匹配:
import type { ElementType, ComponentPropsWithRef } from 'react'; type PolymorphicComponentProps<Props, E extends ElementType> = Props & Omit<ComponentPropsWithRef<E>, keyof Props> & { as?: E; }; type FooOwnProps = { someProp: string; }; type LimitedElementType = Extract<ElementType, 'a' | 'label'>; type FooProps<E extends LimitedElementType = 'a'> = PolymorphicComponentProps<FooOwnProps, E>; function Foo<E extends LimitedElementType = 'a'>({ as = 'a', ...props }: FooProps<E>) { const Com = as; return <Com {...props}>Test</Com>; }
2. 改用React.createElement渲染
如果JSX的类型推导仍有问题,使用createElement可以让TypeScript更准确地推断元素类型:
import { createElement, ElementType, ComponentPropsWithRef } from 'react'; type PolymorphicComponentProps<Props, E extends ElementType> = Props & Omit<ComponentPropsWithRef<E>, keyof Props> & { as?: E; }; type FooOwnProps = { someProp: string; }; type LimitedElementType = Extract<ElementType, 'a' | 'label'>; type FooProps<E extends LimitedElementType = 'a'> = PolymorphicComponentProps<FooOwnProps, E>; function Foo<E extends LimitedElementType = 'a'>({ as = 'a', ...props }: FooProps<E>) { return createElement(as, props, 'Test'); }
3. 显式指定ref类型(复杂场景)
如果需要更精细的控制,可以手动绑定ref到对应元素的DOM类型:
import type { ElementType, ComponentPropsWithRef, Ref } from 'react'; type PolymorphicComponentProps<Props, E extends ElementType> = Props & Omit<ComponentPropsWithRef<E>, keyof Props | 'ref'> & { as?: E; ref?: Ref<HTMLElementTagNameMap[E]>; }; type FooOwnProps = { someProp: string; }; type LimitedElementType = Extract<ElementType, 'a' | 'label'>; type FooProps<E extends LimitedElementType = 'a'> = PolymorphicComponentProps<FooOwnProps, E>; function Foo<E extends LimitedElementType = 'a'>({ as = 'a', ref, ...props }: FooProps<E>) { const Com = as; return <Com ref={ref} {...props}>Test</Com>; }
内容的提问来源于stack exchange,提问作者AmirHossein

