使用Typescript进行反应-使用React.forwardRef时的泛型 [英] React with Typescript -- Generics while using React.forwardRef

查看:118
本文介绍了使用Typescript进行反应-使用React.forwardRef时的泛型的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

I am trying to create a generic component where a user can pass the a custom OptionType to the component to get type checking all the way through. This component also required a React.forwardRef.

我可以在没有forwardRef的情况下使其正常工作.有任何想法吗?下面的代码:

I can get it to work without a forwardRef. Any ideas? Code below:

没有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>
    );
  },
);

App.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 组件不是通用的.有没有办法做到这一点?

In the App.tsx, it says the WithForwardRef component is not generic. Is there a way to achieve this?

回购示例: https://github.com/jgodi/generics-with-forward-ref

谢谢!

推荐答案

不能直接将通用组件创建为 React.forwardRef 的输出 1 (请参阅底部).不过,还有其他选择-让我们简化示例以供说明:

Creating a generic component as output of React.forwardRef is not directly possible 1 (see bottom). There are some alternatives though - let's simplify your example a bit for illustration:

游乐场

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 }
] // just some options data

1.投放

// 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 的返回类型原则上是

This works, as the return type of forwardRef in principle is a plain function. We just need a generic function type shape. You might add an extra type to make the assertion simpler:

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.省略

使用自定义参考道具来代替.这是我最喜欢的-最简单的替代方法,在React 中是合法的方式,不需要 forwardRef .

3. Omit it

Use a custom ref prop instead. This one is my favorite - simplest alternative, a legitimate way in React and doesn't need 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} />


1 为什么原案不起作用?

React.forwardRef 具有以下类型:


1 Why does the original case not work?

React.forwardRef has following type:

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

因此,此函数采用通用类似于渲染函数 ForwardRefRenderFunction ,并返回类型为 ForwardRefExoticComponent 的最终组件.这两个只是函数类型声明

So this function takes a generic component-like render function ForwardRefRenderFunction, and returns the final component with type ForwardRefExoticComponent. These two are just function type declarations with additional properties displayName, defaultProps etc.

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

Now, there is a TypeScript 3.4 feature called higher order function type inference akin to Higher-Rank Types. It basically allows you to propagate free type parameters (generics from the input function) on to the outer, calling function - React.forwardRef here -, so the resulting function component is still generic.

但是,此功能仅适用于普通函数类型,如Anders Hejlsberg在[1] [2] :

But this feature can only work with plain function types, as Anders Hejlsberg explains in [1], [2]:

仅当源类型和目标类型均为纯函数类型(即具有单个调用签名而没有其他成员的类型)时,才进行高阶函数类型推断.

We only make higher order function type inferences when the source and target types are both pure function types, i.e. types with a single call signature and no other members.

如果您不需要 displayName 的类型,请 defaultProps &Co.,您可以增强 React模块类型声明<通过删除其他属性类型,在您的应用中一次.这将使 React.forwardRef 再次与泛型一起使用(有关示例,请参见注释部分).

If you don't need types for displayName, defaultProps & Co., you might augment React module type declarations once in your app by removing the additional property types. This will make React.forwardRef work with generics again (see comment section for an example).

这篇关于使用Typescript进行反应-使用React.forwardRef时的泛型的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆