我正在try 创建一个通用组件,用户可以在其中将自定义OptionType传递给该组件,以便一直进行类型判断.该组件还需要React.forwardRef.

我可以在没有转发的情况下让它工作.有什么 idea 吗?代码如下:

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

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

谢谢

推荐答案

创建一个通用组件作为React.forwardRef的输出是不可能的.不过,也有一些替代方案——让我们简化一下您的示例以进行说明:

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 }
]

为了简单起见, Select 变体(1)或(2).(3) 将用普通props 取代forwardRef.使用(4)你可以在应用程序中全局创建forwardRef个类型定义.

1. Use type assertion ("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的返回类型原则上是plain 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. Omit forwardRef alltogether

custom ref prop代替.这是我最喜欢的——最简单的 Select ,legitimate way in 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.使用全局类型扩充

在应用程序中添加以下代码once,在单独的模块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
}

这将对augment个模块类型声明做出react ,用新的function overload类型签名覆盖forwardRef.权衡:像displayName这样的组件属性现在需要类型断言.


1 Why does the original case not work?

React.forwardRef有以下类型:

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

这个函数接受一个generic的组件,比如render functionForwardRefRenderFunction,然后返回类型为ForwardRefExoticComponent的最终组件.这两个只是函数类型声明with additional propertiesdisplayNamedefaultProps等.

现在,有一个TypeScript 3.4功能,名为higher order function type inference,类似于Higher-Rank Types.它基本上允许您将自由类型参数(来自输入函数的泛型)传播到外部调用函数(这里是React.forwardRef),因此生成的函数组件仍然是泛型的.

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

只有当源类型和目标类型都是纯函数类型时,我们才进行高阶函数类型推断,即具有单个调用签名and no other members的类型.

以上的解决方案将使React.forwardRef个仿制药再次发挥作用.


Playground variants 1, 2, 3

Playground variant 4

Typescript相关问答推荐

返回签名与其他类型正确的函数不同的函数

定义JMX的类型:类型上不存在属性键

Typescript如何从字符串中提取多个文本

根据另一个成员推断类成员的类型

当类型断言函数返回假时,TypScript正在推断从不类型

Angular 17 Select 错误core.mjs:6531错误错误:NG 05105:发现意外合成监听器@transformPanel.done

将props传递给子组件并有条件地呈现它''

在不更改类型签名的情况下在数组并集上映射文字

TypeScrip:逐个 case Select 退出noUncheck kedIndexedAccess

TypeScrip 5.3和5.4-对`Readonly<;[...number[],字符串]>;`的测试版处理:有什么变化?

Select 下拉菜单的占位符不起作用

TypeError:正文不可用-NextJS服务器操作POST

如何使用模板继承从子组件填充基础组件的画布

@TANSTACK/REACT-QUERY中发生Mutations 后的数据刷新问题

在Cypress中,您能找到表格中具有特定文本的第一行吗?

是否将Angular *ngFor和*ngIf迁移到新的v17语法?

如何在Type脚本中创建一个只接受两个对象之间具有相同值类型的键的接口?

如何在TypeScript类成员函数中使用筛选后的keyof this?

如何知道使用TypeScript时是否在临时工作流内运行

如何在由函数参数推断的记录类型中具有多个对象字段类型