与 Typescript react——使用 React.forwardRef 时的泛型

IT技术 reactjs typescript generics
2021-04-04 00:42:47

我正在尝试创建一个通用组件,用户可以在其中将自定义传递OptionType给组件以进行类型检查。该组件还需要一个React.forwardRef.

我可以让它在没有 forwardRef 的情况下工作。有任何想法吗?代码如下:

无转发引用.tsx

export interface Option<OptionValueType = unknown> {
  value: OptionValueType;
  label: string;
}

interface WithoutForwardRefProps<OptionType> {
  onChange: (option: OptionType) => void;
  options: OptionType[];
}

export const WithoutForwardRef = <OptionType extends Option>(
  props: WithoutForwardRefProps<OptionType>,
) => {
  const { options, onChange } = props;
  return (
    <div>
      {options.map((opt) => {
        return (
          <div
            onClick={() => {
              onChange(opt);
            }}
          >
            {opt.label}
          </div>
        );
      })}
    </div>
  );
};

WithForwardRef.tsx

import { Option } from './WithoutForwardRef';

interface WithForwardRefProps<OptionType> {
  onChange: (option: OptionType) => void;
  options: OptionType[];
}

export const WithForwardRef = React.forwardRef(
  <OptionType extends Option>(
    props: WithForwardRefProps<OptionType>,
    ref?: React.Ref<HTMLDivElement>,
  ) => {
    const { options, onChange } = props;
    return (
      <div>
        {options.map((opt) => {
          return (
            <div
              onClick={() => {
                onChange(opt);
              }}
            >
              {opt.label}
            </div>
          );
        })}
      </div>
    );
  },
);

应用程序.tsx

import { WithoutForwardRef, Option } from './WithoutForwardRef';
import { WithForwardRef } from './WithForwardRef';

interface CustomOption extends Option<number> {
  action: (value: number) => void;
}

const App: React.FC = () => {
  return (
    <div>
      <h3>Without Forward Ref</h3>
      <h4>Basic</h4>
      <WithoutForwardRef
        options={[{ value: 'test', label: 'Test' }, { value: 1, label: 'Test Two' }]}
        onChange={(option) => {
          // Does type inference on the type of value in the options
          console.log('BASIC', option);
        }}
      />
      <h4>Custom</h4>
      <WithoutForwardRef<CustomOption>
        options={[
          {
            value: 1,
            label: 'Test',
            action: (value) => {
              console.log('ACTION', value);
            },
          },
        ]}
        onChange={(option) => {
          // Intellisense works here
          option.action(option.value);
        }}
      />
      <h3>With Forward Ref</h3>
      <h4>Basic</h4>
      <WithForwardRef
        options={[{ value: 'test', label: 'Test' }, { value: 1, label: 'Test Two' }]}
        onChange={(option) => {
          // Does type inference on the type of value in the options
          console.log('BASIC', option);
        }}
      />
      <h4>Custom (WitForwardRef is not generic here)</h4>
      <WithForwardRef<CustomOption>
        options={[
          {
            value: 1,
            label: 'Test',
            action: (value) => {
              console.log('ACTION', value);
            },
          },
        ]}
        onChange={(option) => {
          // Intellisense SHOULD works here
          option.action(option.value);
        }}
      />
    </div>
  );
};

在 中App.tsx,它表示该WithForwardRef组件不是通用的。有没有办法实现这一目标?

示例存储库:https : //github.com/jgodi/generics-with-forward-ref

谢谢!

1个回答

React.forwardRef不能直接创建通用组件作为输出1(见底部)。虽然有一些替代方案 - 让我们稍微简化一下您的示例以进行说明:

type Option<O = unknown> = { value: O; label: string; }
type Props<T extends Option<unknown>> = { options: T[] }

const options = [
  { value: 1, label: "la1", flag: true }, 
  { value: 2, label: "la2", flag: false }
]

为简单起见,选择变体 (1) 或 (2)。(3) 将替换forwardRef为通常的props。使用 (4) 您可以forwardRef在应用程序中全局设置一次类型定义。

1.使用类型断言(“cast”)

// Given render function (input) for React.forwardRef
const FRefInputComp = <T extends Option>(p: Props<T>, ref: Ref<HTMLDivElement>) =>
  <div ref={ref}> {p.options.map(o => <p>{o.label}</p>)} </div>

// Cast the output
const FRefOutputComp1 = React.forwardRef(FRefInputComp) as
  <T extends Option>(p: Props<T> & { ref?: Ref<HTMLDivElement> }) => ReactElement

const Usage11 = () => <FRefOutputComp1 options={options} ref={myRef} />
// options has type { value: number; label: string; flag: boolean; }[] 
// , so we have made FRefOutputComp generic!

这是有效的,因为forwardRef原则上的返回类型是一个普通的 function我们只需要一个通用的函数类型形状。您可以添加额外的类型以使断言更简单:

type ForwardRefFn<R> = <P={}>(p: P & React.RefAttributes<R>) => ReactElement |null
// `RefAttributes` is built-in type with ref and key props defined
const Comp12 = React.forwardRef(FRefInputComp) as ForwardRefFn<HTMLDivElement>
const Usage12 = () => <Comp12 options={options} ref={myRef} />

2. 包装转发组件

const FRefOutputComp2 = React.forwardRef(FRefInputComp)
// ↳ T is instantiated with base constraint `Option<unknown>` from FRefInputComp

export const Wrapper = <T extends Option>({myRef, ...rest}: Props<T> & 
  {myRef: React.Ref<HTMLDivElement>}) => <FRefOutputComp2 {...rest} ref={myRef} />

const Usage2 = () => <Wrapper options={options} myRef={myRef} />

3.省略forwardRef产品总数

改用自定义 ref props这是我最喜欢的 - 最简单的替代方案,是React 中的一种合法方式,不需要forwardRef.

const Comp3 = <T extends Option>(props: Props<T> & {myRef: Ref<HTMLDivElement>}) 
  => <div ref={myRef}> {props.options.map(o => <p>{o.label}</p>)} </div>
const Usage3 = () => <Comp3 options={options} myRef={myRef} />

4. 使用全局类型扩充

在您的应用程序中添加以下代码一次,最好在单独的module中react-augment.d.ts

import React from "react"

declare module "react" {
  function forwardRef<T, P = {}>(
    render: (props: P, ref: ForwardedRef<T>) => ReactElement | null
  ): (props: P & RefAttributes<T>) => ReactElement | null
}

这将增加React module类型声明,覆盖forwardRef一个新的函数重载类型签名。权衡:像displayName现在这样的组件属性需要类型断言。


1为什么原来的case不行?

React.forwardRef 有以下类型:

function forwardRef<T, P = {}>(render: ForwardRefRenderFunction<T, P>): 
  ForwardRefExoticComponent<PropsWithoutRef<P> & RefAttributes<T>>;

所以这个函数接受一个类似组件通用渲染函数 ForwardRefRenderFunction,并返回类型为的最终组件ForwardRefExoticComponent这两个都只是函数的类型声明与附加属性 displayNamedefaultProps等等。

现在,有一个 TypeScript 3.4 功能称为高阶函数类型推断,类似于Higher-Rank Types它基本上允许您将自由类型参数(来自输入函数的泛型)传播到外部调用函数 -React.forwardRef此处 -,因此生成的函数组件仍然是泛型的。

但是这个特性只能用于普通的函数类型,正如 Anders Hejlsberg 在[1][2] 中解释的那样

我们只在源类型和目标类型都是纯函数类型时才进行高阶函数类型推断,即具有单个调用签名且没有其他成员的类型

以上解决方案将React.forwardRef再次使用泛型。


游乐场变体 1、2、3

游乐场变体 4

注意:有一个第四替代:扩充反应类型的forwardRef使用的通用功能(类似于例如React.memo
2021-05-30 00:42:47
@ford04 非常感谢您的解决方案!很干净的解释。为了将来参考,我创建了一个小代码沙箱,以便人们可以看到您的解决方案的实际应用。
2021-06-04 00:42:47
非常感谢福特04。你的回答对我帮助很大。
2021-06-18 00:42:47
@arcety 我只是在本地环境中尝试过 - 到目前为止似乎有效。看看这里并将其粘贴到本地环境中(至少对我来说,playground 目前不喜欢 TS + React)。还要注意返回类型(props: P & RefAttributes<T>) => ReactElement | null而不是ForwardRefExoticComponent. 除了单个调用签名之外还具有属性的函数类型确实将类型参数推断为unknown,我想这就是它的实现方式。干杯
2021-06-20 00:42:47
@arcety 很高兴它有效。如果您不需要其他属性的类型(例如defaultProps等),则此替代方法应该没问题。而且您只需要在应用程序中执行一次。我用更好的解释更新了答案。
2021-06-20 00:42:47