用类型脚本进行反应——使用 React.forwardRef 时的泛型

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

我可以让它工作,没有向前裁判。有什么想法吗? 代码如下:

没有转发

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>
);
};

转发

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

谢谢!

28055 次浏览

创建一个通用组件作为 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。与(4)你全局机会 forwardRef类型定义一次在应用程序。

游乐场变种1,2,3

游乐场版本4

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的返回类型原则上是 普通功能。我们只需要一个通用函数类型的形状。您可以添加一个额外的类型来简化断言:

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

使用 定制裁判道具代替。这是我最喜欢的-最简单的选择,一个 合法的反应方式和不需要 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. 使用全局类型增强

添加以下代码 一次在您的应用程序,最好在一个单独的模块 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
}

这将使用新的 功能过载类型签名覆盖 forwardRef,从而实现 增强 React 模块类型声明。折衷: 像 displayName这样的组件属性现在需要一个类型断言。


为什么原来的情况不起作用?

React.forwardRef 具有以下类型:

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

因此,该函数接受类似于 呈现函数 ForwardRefRenderFunction一般组件,并返回类型为 ForwardRefExoticComponent的最终组件。这两个只是函数类型声明 附加财产displayNamedefaultProps等。

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

但是这个功能只适用于普通的功能类型,正如安德斯·海尔斯伯格在 [1][2]中所解释的:

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

以上解决方案将使 React.forwardRef再次与泛型一起工作。

我在阅读 这篇博文时发现了这个问题,而且我认为有一种比目前 接受的答案更直接的方法来解决这个问题:

首先,我们定义一个接口来保存组件的类型,使用类型脚本中的 呼叫信号:

interface WithForwardRefType extends React.FC<WithForwardRefProps<Option>>  {
<T extends Option>(props: WithForwardRefProps<T>): ReturnType<React.FC<WithForwardRefProps<T>>>
}

请注意函数签名本身是如何声明为泛型的,而不是接口-这是使其工作的关键。该接口还扩展了 React.FC,以便公开一些有用的 Component 属性,如 displayNamedefaultProps等。

接下来,我们只需提供接口作为组件的类型,然后 不需要指定道具的类型,我们可以将该组件传递给 forwardRef,剩下的就是历史..。

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

沙盒链接 给你


参考文献: