简介
我正在尝试为React Native
中的Dialog
组件创建Higher Order Component
。遗憾的是,我有一些编译错误,我根本不理解。我已经在TypeScript
中学习了关于Higher Order Components
的教程,但它没有显示让ref
工作的示例。
设置
我有一个名为DialogLoading
的组件,并通过一个称为withActions
的Higher Order Component
导出它。withActions
组件定义了两个接口,用于确定它注入了什么道具和接受了什么额外道具。在下面的代码中,类型参数C
、A
和P
分别代表ComponentType
、ActionType
和PropType
。
接口为:
interface InjectedProps<A>
{ onActionClicked: (action: A) => void;}
和
interface ExternalProps<C, A>
{ onActionClickListener?: (component: C | null, action: A) => void;}
我还声明了一个类型别名,表示HOC
的最终道具类型。该类型应该具有封装组件的所有props,ExternalProps<C, A>
接口的所有prop,但不具有InjectedProps<A>
接口的props。声明如下:
type Omit<T, K> = Pick<T, Exclude<keyof T, K>>;
type Subtract<T, K> = Omit<T, keyof K>;
type HocProps<C, A, P extends InjectedProps<A>> = Subtract<P, InjectedProps<A>> & ExternalProps<C, A>;
Higher Order Component
随后声明如下:
export default <C, A, P extends InjectedProps<A>> (WrappedComponent: React.ComponentType<P>) =>
{
const hoc = class WithActions extends React.Component<HocProps<C, A, P>>
{
...Contents of class removed for breivity.
private onActionClicked = (action: A) =>
{
this.onActionClickedListeners.forEach(listener =>
{ listener(this.wrapped, action);});
}
private wrapped: C | null;
render()
{
return (
<WrappedComponent ref={i => this.wrapped = i} onActionClicked={this.onActionClicked} {...this.props} />
);
}
}
return hoc;
}
可用于:
<DialogLoading onActionClickListener={this.onActionClickListener} title="Loading Data" section="Connecting" />;
问题
在HOC
的呈现函数内部的ref回调上,TypeScript
给了我以下错误消息:
[ts] Property 'ref' does not exist on type 'IntrinsicAttributes & InjectedProps<A> & { children?: ReactNode; }'.
[ts] Type 'Component<P, ComponentState, never> | null' is not assignable to type 'C | null'.
Type 'Component<P, ComponentState, never>' is not assignable to type 'C'.
我怀疑这是因为传入的WrappedComponent
的类型是React.ComponentType<P>
,它是React.ComponentClass<P>
和React.SFC<P>
的并集类型。由于React中的无状态组件不接受ref回调,因此引发了错误。一个可能的解决方案是将其类型更改为仅React.ComponentClass<P>
。
这种类型的修复了问题,但奇怪的是,现在在封装组件的onActionClicked
道具上抛出了一个新错误!错误为:
[ts] Type '(action: A) => void' is not assignable to type '(IntrinsicAttributes & IntrinsicClassAttributes<Component<P, ComponentState, never>> & Readonly<{ children?: ReactNode; }> & Readonly<P>)["onActionClicked"]'.
WithActions.tsx(7, 5): The expected type comes from property 'onActionClicked' which is declared here on type 'IntrinsicAttributes & IntrinsicClassAttributes<Component<P, ComponentState, never>> & Readonly<{ children?: ReactNode; }> & Readonly<P>'
第二个错误让我完全困惑。更奇怪的是,当我将HocProps
的类型别名调整为以下值时(即,我不再从P
中减去InjectedProps<A>
):
type HocProps<C, A, P extends InjectedProps<A>> = P & ExternalProps<C, A>;
onActionClicked
上的错误已删除!这对我来说似乎很奇怪,因为HocProps
的类型定义与封装组件的道具类型无关!然而,这种"解决方案"对我来说是不可取的,因为现在InjectedProps<A>
也可以由HOC
的用户注射。
问题
那么我哪里错了?
因为
Wrapped Component
类型是React.ComponentType<P>
而不是React.ComponentClass<P>
,所以我假设ref回调不起作用,这是正确的吗?为什么将
Wrapped Component
类型更改为React.ComponentClass<P>
会导致Wrapped Component
的onActionClicked
道具出现编译错误?为什么更改
HocProps
的类型别名会删除onActionClicked
道具上的错误?他们不是完全没有关系吗?我准备的
Subtract
功能正确吗?这就是错误的来源吗?
如有任何帮助,我们将不胜感激,因此提前感谢!
因为
Wrapped Component
类型是React.ComponentType<P>
而不是React.ComponentClass<P>
,所以我假设ref回调不起作用,这是正确的吗?
粗略地说,是的。当您从具有并集类型(React.ComponentType<P> = React.ComponentClass<P> | React.StatelessComponent<P>
)的WrappedComponent
生成JSX元素时,TypeScript会找到与并集的每个备选方案相对应的props类型,然后使用子类型约简获取props类型的并集。来自checker.ts(太大,无法链接到GitHub上的行):
function resolveCustomJsxElementAttributesType(openingLikeElement: JsxOpeningLikeElement,
shouldIncludeAllStatelessAttributesType: boolean,
elementType: Type,
elementClassType?: Type): Type {
if (elementType.flags & TypeFlags.Union) {
const types = (elementType as UnionType).types;
return getUnionType(types.map(type => {
return resolveCustomJsxElementAttributesType(openingLikeElement, shouldIncludeAllStatelessAttributesType, type, elementClassType);
}), UnionReduction.Subtype);
}
我不知道为什么这是规则;交叉点对于确保所有所需的道具都存在更有意义,无论组件是并集的哪一个替代方案。在我们的示例中,React.ComponentClass<P>
的道具类型包括ref
,而React.StatelessComponent<P>
的道具类型不包括。通常,如果一个属性至少存在于并集的一个组成部分中,则该属性被认为是并集类型的"已知"属性。然而,在本例中,子类型缩减抛出了React.ComponentClass<P>
的props类型,因为它恰好是React.StatelessComponent<P>
的props型的子类型(具有比该类型更多的属性),所以我们只剩下React.StatelessComponent<P>
,它没有ref
属性。同样,这一切看起来很奇怪,但它产生了一个错误,指向代码中的一个实际错误,所以我不倾向于针对TypeScript提交错误。
为什么将
Wrapped Component
类型更改为React.ComponentClass<P>
会导致Wrapped Component
的onActionClicked
道具出现编译错误?
此错误的根本原因是TypeScript无法确定类型为Readonly<HocProps<C, A, P>> & { onActionClicked: (action: A) => void; }
的组合onActionClicked={this.onActionClicked} {...this.props}
提供所需道具类型P
的原因。您的意图是,如果从P
中减去onActionClicked
,然后再加回来,则应该只剩下P
,但TypeScript没有内置规则来验证这一点。(P
可能会声明一个类型为(action: A) => void
子类型的onActionClicked
属性,这是一个潜在的问题,但您的使用模式非常常见,我预计如果TypeScript添加这样的规则,该规则会以某种方式绕过这个问题。)
TypeScript 3.0.3报告onActionClicked
上的错误令人困惑(尽管这可能是由于我提到的问题)。我进行了测试,在3.0.3和3.2.0-dev.20180926之间的某个时间点,行为发生了变化,在WrappedComponent
上报告了错误,这似乎更合理,因此这里不需要进一步的跟进。
当WrappedComponent
类型为React.ComponentType<P>
时,错误不会发生的原因是,对于无状态函数组件(与组件类不同),TypeScript只检查您是否传递了足够的道具来满足道具类型P
的约束,即InjectedProps<A>
,而不是实际的P
。我认为这是一个错误,并已报告。
为什么更改
HocProps
的类型别名会删除onActionClicked
道具上的错误?他们不是完全没有关系吗?
因为{...this.props}
本身满足所需的P
。
我编写的
Subtract
功能是否正确?这就是错误的来源吗?
您的Subtract
是正确的,但如上所述,TypeScript几乎不支持对底层Pick
和Exclude
进行推理。
为了解决您最初的问题,我建议使用类型别名和交集,而不是本答案中描述的减法。在您的情况下,这看起来像:
import * as React from "react";
interface InjectedProps<A>
{ onActionClicked: (action: A) => void;}
interface ExternalProps<C, A>
{ onActionClickListener?: (component: C | null, action: A) => void;}
// See https://stackoverflow.com/a/52528669 for full explanation.
const hocInnerPropsMarker = Symbol();
type HocInnerProps<P, A> = P & {[hocInnerPropsMarker]?: undefined} & InjectedProps<A>;
type HocProps<C, A, P> = P & ExternalProps<C, A>;
const hoc = <C extends React.Component<HocInnerProps<P, A>>, A, P>
(WrappedComponent: {new(props: HocInnerProps<P, A>, context?: any): C}) =>
{
const hoc = class WithActions extends React.Component<HocProps<C, A, P>>
{
onActionClickedListeners; // dummy declaration
private onActionClicked = (action: A) =>
{
this.onActionClickedListeners.forEach(listener =>
{ listener(this.wrapped, action);});
}
private wrapped: C | null;
render()
{
// Workaround for https://github.com/Microsoft/TypeScript/issues/27484
let passthroughProps: Readonly<P> = this.props;
let innerProps: Readonly<HocInnerProps<P, A>> = Object.assign(
{} as {[hocInnerPropsMarker]?: undefined},
passthroughProps, {onActionClicked: this.onActionClicked});
return (
<WrappedComponent ref={i => this.wrapped = i} {...innerProps} />
);
}
}
return hoc;
}
interface DiagLoadingOwnProps {
title: string;
section: string;
}
// Comment out the `{[hocInnerPropsMarker]?: undefined} &` in `HocInnerProps`
// and uncomment the following two lines to see the inference mysteriously fail.
//type Oops1<T> = DiagLoadingOwnProps & InjectedProps<string>;
//type Oops2 = Oops1<number>;
class DiagLoadingOrig extends React.Component<
// I believe that the `A` type parameter is foiling the inference rule that
// throws out matching constituents from unions or intersections, so we are
// left to rely on the rule that matches up unions or intersections that are
// tagged as references to the same type alias.
HocInnerProps<DiagLoadingOwnProps, string>,
{}> {}
const DialogLoading = hoc(DiagLoadingOrig);
class OtherComponent extends React.Component<{}, {}> {
onActionClickListener;
render() {
return <DialogLoading onActionClickListener={this.onActionClickListener} title="Loading Data" section="Connecting" />;
}
}
(这里我还更改了WrappedComponent
的类型,使其实例类型为C
,以便对this.wrapped
类型的赋值进行检查。)