React和Typescript:如何扩展通用组件道具?

时间:2020-10-11 14:27:47

标签: reactjs typescript generics react-component

想象一下一个灵活的组件,它需要一个React.ComponentType及其props并进行渲染:

type Props<C> = {
  component: React.ComponentType<C>;
  componentProps: C;
  otherProp: string;
};

const MyComponent = <C extends {}>(props: Props<C>) => {
  return React.createElement(props.component, props.componentProps);
};

我是否可以让MyComponent直接接收动态props,例如那样(不起作用):

type Props<C> = {
  component: React.ComponentType<C>;
  otherProp: string;
};

const MyComponent = <C extends {}>(props: Props<C> & C) => {
  const { otherProp, component, ...componentProps } = props;
  return React.createElement(component, componentProps);
};

错误:

Error:(11, 41) TS2769: No overload matches this call.
  The last overload gave the following error.
    Argument of type 'Pick<Props<C> & C, Exclude<keyof C, "component" | "otherProp">>' is not assignable to parameter of type 'Attributes & C'.
      Type 'Pick<Props<C> & C, Exclude<keyof C, "component" | "otherProp">>' is not assignable to type 'C'.
        'Pick<Props<C> & C, Exclude<keyof C, "component" | "otherProp">>' is assignable to the constraint of type 'C', but 'C' could be instantiated with a different subtype of constraint '{}'.

1 个答案:

答案 0 :(得分:1)

在这里,我们需要了解一些实用程序类型以及在TS中如何进行销毁。

type Obj = {
  [key: string]: any
}

interface I1 {
  a: number
  b: number
  c: number
}

const i1: I1 = {
  a: 1,
  b: 1,
  c: 1,
}

let {a, ...rest} = i1

interface Obj {
    [key: string]: any
}

const i2: Obj & I1 = {
  a: 1,
  b: 1,
  c: 1,
  d: 1,
  e: 1,
}

let {a: a1, b, c, ...rest2} = i2

function func<T extends Obj>(param: I1 & T) {
    const {a, b, c, ...rest} = param
}

在上面的代码中,rest的推断类型将为{b: number, c: number},因为对象i1仅包含三个键,并且其中一个也称为a。对于rest2,由于接口Obj的键已用尽,TS仍可以推断类型为I1。精疲力竭是指没有使用rest运算符捕获它们。

但是在功能上,TS无法执行这种类型的推断。我不知道TS无法执行的原因。这可能是由于通用类的限制。

在使用函数的情况下,函数内部rest的类型为Pick<I1 & T, Exclude<keyof T, "a" | "b" | "c">>Exclude从通用类型a中排除键bcT。选中排除here。然后,Pick使用I1 & T返回的键从Exclude创建新类型。由于T可以是任何类型,因此即使T约束为Obj,TS也无法确定排除后的密钥,因此也无法确定所选择的密钥以及新创建的类型。这就是为什么函数中的类型变量rest保持Pick<I1 & T, Exclude<keyof T, "a" | "b" | "c">>的原因。

请注意,Pick返回的类型是Obj的子类型

现在要问的是,componentProps也会发生相同的情况。推断的类型将为Pick<Props<C> & C, Exclude<keyof C, "otherProp" | "component">>。 TS将无法缩小范围。查看React.createElement

的签名
 function createElement<P extends {}>(
        type: ComponentType<P> | string,
        props?: Attributes & P | null,
        ...children: ReactNode[]): ReactElement<P>

并称呼它

React.createElement(component, componentProps)

在签名中,P的推断类型将在代码中从第一个参数开始C,即component,因为它的类型为React.ComponentType<C>。第二个参数应该是undefinednullC(到目前为止,忽略Attributes)。但是componentProps的类型为Pick<Props<C> & C, Exclude<keyof C, "otherProp" | "component">>,它绝对可以分配给{}而不是C,因为它是{}的子类型,而不是{{1} }。 C也是C的子类型,但选择类型和{}可能兼容,也可能不兼容(这与-存在类A; B和C派生A,对象B和C的B可以分配给A,但是B的对象不能归于C)。这就是为什么错误

C

由于我们比TS编译器更智能,因此我们知道它们兼容,但TS不兼容。因此,让TS相信我们做的正确,我们可以像这样进行类型断言

        'Pick<Props<C> & C, Exclude<keyof C, "component" | "otherProp">>' is assignable to the constraint of type 'C', but 'C' could be instantiated with a different subtype of constraint '{}'.

这绝对是正确的类型断言,因为我们知道type Props<C> = { component: React.ComponentType<C>; otherProp: string; }; const MyComponent = <C extends {}>(props: Props<C> & C) => { const { otherProp, component, ...componentProps } = props; return React.createElement(component, componentProps as unknown as C); // ------------------------------------------------^^^^^^^^ }; 的类型将是componentProps

希望这能回答您的问题并解决您的问题。