【问题标题】:React TypeScript - Pass a dynamic generic type into a forwardRef componentReact TypeScript - 将动态泛型类型传递给 forwardRef 组件
【发布时间】:2021-11-30 04:19:43
【问题描述】:

我的问题的核心

const FinalComponent<GenericType extends 'a' | 'b'> = 是无效的 tsx 语法。

// The 1st line here is invalid tsx syntax
const FinalComponent<InvalidGenericType extends 'a' | 'b'> = 
    forwardRef<HTMLParagraphElement, PropsWithStandardRef<InvalidGenericType>>(({ value }, ref) => {
        return <Component forwardedRef={ref} value={value} />
    }) as ComponentType<InvalidGenericType>

组件的预期用途:

const ExampleUsage = () => <FinalComponent<'b'> value="b" />

在这种情况下如何创建泛型类型?


附加上下文

有关其他上下文,以下是其余代码:

import { Ref, forwardRef } from 'react'

// These are the base props for the component. 
// In terms of usage, these are the props that I care about.
interface Props<GenericType extends 'a' | 'b'> {
    value: GenericType
}

// Adding forwardedRef to the props to define what props are usable inside the component
interface PropsWithForwardRef<GenericType extends 'a' | 'b'> extends Props<GenericType> {
    forwardedRef: Ref<HTMLParagraphElement | null>
}

// Adding standard ref to the props to define what props the component can accept from outside
interface PropsWithStandardRef<GenericType extends 'a' | 'b'> extends Props<GenericType> {
    ref?: Ref<HTMLParagraphElement | null>
}

// forwardRef is interfering with the inheritance of the generic types.
// This is a stand in for the expected return type of the component.
type ComponentType<GenericType extends 'a' | 'b'> = (props: PropsWithStandardRef<GenericType>) => JSX.Element

// The core component code
function CoreComponent<GenericType extends 'a' | 'b'> ({ value, forwardedRef }:PropsWithForwardRef<GenericType>):JSX.Element {
    return <p ref={forwardedRef}>{value}</p>
}

// !!!!!!!!!!! IMPORTANT BIT !!!!!!!!!!!!
// This is where my problem is, I need to be able to pass a dynamic generic type into PropsWithStandardRef and ComponentType.
// I'm not sure how to do that though because `const FinalComponent<InvalidGenericType extends 'a' | 'b'> = forwardRef()` is invalid
const FinalComponent<InvalidGenericType extends 'a' | 'b'> = forwardRef<HTMLParagraphElement, PropsWithStandardRef<InvalidGenericType>>(({ value }, ref) => {

    return <CoreComponent forwardedRef={ref} value={value} />

    // I need the `as ComponentType<InvalidGenericType>` bit because the inferred type that comes out of forwardRef
    // is making TS lose the generic types information
}) as ComponentType<InvalidGenericType>


// This is the end goal of how I want to be able to use this component
// I want to be able to pass a generic type into the component without TS complaining
const ExampleUsage = () => <FinalComponent<'b'> value="b" />

PS。我承认这个例子有点做作,它是为了简化我的现实世界问题,它具有更复杂的组件。


类似但不同的问题

这与React Typescript - dynamic types不同

在那个问题中,它不需要将类型信息传递给变量,而只是根据用户提供的值来改变类型。

我需要组件的最终用途才能将类型传递给它。

【问题讨论】:

    标签: reactjs typescript


    【解决方案1】:

    您提供的代码非常接近工作,但是为了类型正确性,需要更改一些内容:

    1. 包含 HTML 元素的 Refs 不应该是可变的/可空的,因为它们是由 React 设置和管理的,并且 Ref&lt;T&gt; util 已经包含 null 无论如何。 (除非你正在做一些真正奇特的事情,比如在渲染树之外强制操作元素......但我什至从未在代码库中看到过这种情况。)因此,我在 @987654325 中从你的联合中删除了 null @。 (这也会导致将值传递给 CoreComponent 中的实际段落元素时出现问题。)

    2. ComponentType 的返回类型需要包含null,以便forwardRef 的返回类型可以分配给它。说到返回 React 元素的函数的返回类型,JSX.Element 只是 ReactElement 的别名,any 在类型参数中传递。我将JSX.Element 引用更改为ReactElement

    3. 类型注释仍然可以应用于保存函数表达式值的变量。它的编写方式与任何其他注释的编写方式相同:在标识符名称之后,如下所示:

      const add: (...numbers: number[]) => number = (...nums) => nums.reduce((sum, n) => sum + n, 0);
      

      在我看来,上面的语法不容易阅读,所以我更喜欢将类型括在括号中以提高可读性。您甚至可以使用带有签名的泛型(在您的情况下需要),并且重载也是可能的。请参阅下面代码中的FinalComponent,已修改:

    TS Playground link

    import {
      default as React,
      createRef,
      forwardRef,
      ReactElement,
      Ref,
    } from 'react';
    
    type AorB = 'a' | 'b';
    
    type Props<T extends AorB> = { value: T };
    type PropsWithForwardRef<T extends AorB> = Props<T> & { forwardedRef: Ref<HTMLParagraphElement> };
    type PropsWithStandardRef<T extends AorB> = Props<T> & { ref?: Ref<HTMLParagraphElement> };
    
    function CoreComponent<T extends AorB> ({ value, forwardedRef }:PropsWithForwardRef<T>): ReactElement {
      return <p ref={forwardedRef}>{value}</p>;
    }
    
    const FinalComponent: (<T extends AorB>(props: PropsWithStandardRef<T>) => ReactElement | null) =
      forwardRef<HTMLParagraphElement, Props<AorB>>(({ value }, ref) => <CoreComponent forwardedRef={ref} value={value} />);
    
    /**
     * The annotation for the function expression above can also be written this way,
     * which allows for overloading with multiple signatures, one on each line inside the braces:
     */
    // const FinalComponent: {
    //   <T extends AorB>(props: PropsWithStandardRef<T>): ReactElement | null;
    // } = forwardRef<HTMLParagraphElement, Props<AorB>>(({ value }, ref) => <CoreComponent forwardedRef={ref} value={value} />);
    
    /* Use: */
    
    const ref = createRef<HTMLParagraphElement>();
    
    const ExampleA = () => <FinalComponent<'a'> value="a" ref={ref} />;
    const ExampleB = () => <FinalComponent<'b'> value="b" ref={ref} />;
    const RefOptional = () => <FinalComponent<'a'> value="a" />;
    const NoRestrictionA = () => <FinalComponent value="a" ref={ref} />;
    const NoRestrictionB = () => <FinalComponent value="b" />;
    
    const InvalidA = () => <FinalComponent<'a'> value="b" ref={ref} />;
    const InvalidNotAorB = () => <FinalComponent value="c" />;
    const InvalidNoValue = () => <FinalComponent<'a'> />;
    

    【讨论】:

    • 您提到想要一个“规范”的答案。我认为规范是 React 的类型声明文件(它总是在不断发展)。如果您需要我链接到类型声明中的某些地方,我可以搜索并添加行号,但您似乎已经熟悉搜索 React 类型,因为您了解钩子、组件等所需的参数。
    • 为了解决我的问题,提供最丰富的信息,并教给我关于 TS 的东西,我什至没有在问题中要求,我认为你应该得到赏金 :) 为了进一步改进你的答案,如果您可以使用FinalComponent 代码在您的帖子顶部添加一个“简短答案”部分,然后在下方提供您现有答案的“长答案”部分,那就太好了。这样一来,对于寻求快速解决方案的人来说,您的答案就不那么令人生畏了。
    • 我猜赏金信息无法再阅读,但 IIRC 表示这是您的团队在工作中采用 TypeScript 的最后一个障碍。我很高兴现在可以了!恭喜您在工作中获得更好的编程体验。
    • 是的,这使我无法将我们的通用组件设计系统转换为 TS。在可以转换通用组件之前,不能将其他项目转换为 TS。不过,它花了 3 天的时间没有得到答复,这就是为什么我非常绝望地发布了 500 个代表的赏金。
    • 我使用了您注释掉的语法。我认为它更容易阅读。
    【解决方案2】:

    变量不能使用泛型。将其定义为 FC:

    const FinalComponent: <InvalidGenericType extends 'a' | 'b'>(
      props: PropsWithStandardRef<InvalidGenericType>
    ) => JSX.Element = forwardRef<
      HTMLParagraphElement,
      PropsWithStandardRef<'a' | 'b'>
    >(({ value }, ref) => {
      return <CoreComponent forwardedRef={ref} value={value} />
    }) as ComponentType<'a' | 'b'>
    

    或者使用接口来定义它:

    interface FinalComponentType
      extends React.ForwardRefExoticComponent<PropsWithStandardRef<'a' | 'b'>> {
      <GenericType extends 'a' | 'b'>(
        props: PropsWithStandardRef<GenericType>
      ): JSX.Element
    }
    
    const FinalComponent = forwardRef<
      HTMLParagraphElement,
      PropsWithStandardRef<'a' | 'b'>
    >(({ value }, ref) => {
      return <CoreComponent forwardedRef={ref} value={value} />
    }) as FinalComponentType
    

    工作正常:

    const ExampleUsage = () => <FinalComponent<'a'> value="b" /> // There will be error
    

    【讨论】:

      【解决方案3】:

      我已经多次遇到这个问题,虽然这不是一个规范的答案,但我通常是这样解决的:

      您基本上需要您的反应函数组件来接受泛型类型作为类型参数。我发现处理这个问题的唯一方法是创建函数组件而不明确将其声明为函数组件。问题是:

      const FinalComponent : React.FC<Props<GenericType<...>> = // There's no type variable that you can use at that point
      

      要绕过这个,我要做的是:

      const FinalComponent = <T extends 'a'|'b'>(props: Props<T>): ReturnType<React.FC<Props<T>>> => null; //Return whatever you need here
      

      这将声明一个通用的反应函数组件,您可以像下面这样使用它:

      const Res = () => <FinalComponent<'a'> value='a' />; // Works
      const Res2 = () => <FinalComponent<'a'> value='b' />; // Errors 
      

      Random link

      【讨论】:

      • 在您的回答中使用 React.FC 会取消您的赏金资格。 React.FC 阻止我们声明是否允许/需要儿童。
      • 这里我使用FC作为返回类型。但是,据我所知,打字稿无法阻止孩子通过。没有办法在&lt;FinalComponent&lt;'a'&gt;&gt;&lt;Child /&gt;&lt;/FinalComponent&gt;上触发错误
      • 如果你在 props 类型接口中使用children: NonNullable&lt;ReactNode&gt;,那么如果组件没有子组件,TS 会抛出这个错误:"Property 'children' is missing in type '{}' but required in type '道具'”。但是,如果您使用 React.FC,则不会获得这种级别的类型特异性。
      猜你喜欢
      • 1970-01-01
      • 2018-07-07
      • 2019-01-24
      • 1970-01-01
      • 1970-01-01
      • 2017-12-23
      • 2016-02-12
      • 1970-01-01
      相关资源
      最近更新 更多