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

React and Typescript: How to extend generic component props?

想象一个灵活的组件,它接受 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 '{}'.

这里我们需要了解一些实用类型以及解构在 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 的情况下,TS 仍然可以将类型推断为 Obj,因为来自接口 I1 的键已用完。精疲力尽的意思是它们不是使用 rest 运算符捕获的。

但在函数的情况下,TS 无法进行此类推理。我不知道为什么TS做不到。这可能是由于泛型的限制。

在函数的情况下发生的情况是函数内部 rest 的类型是 Pick<I1 & T, Exclude<keyof T, "a" | "b" | "c">>Exclude 从通用类型 T 中排除键 abc。选中排除 here。然后,Pick 使用 Exclude 返回的键从 I1 & T 创建一个新类型。由于 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 的推断类型在您的代码中将从第一个参数即 component 推断为 C,因为它的类型为 React.ComponentType<C>。第二个参数应该是 undefinednullC(现在忽略 Attributes)。但是 componentProps 的类型是 Pick<Props<C> & C, Exclude<keyof C, "otherProp" | "component">>,它肯定可以分配给 {} 但不能分配给 C 因为它是 {} 的子类型而不是 C. C 也是 {} 的子类型,但选择类型和 C 可能兼容也可能不兼容(这与 - 有一个 class A;B 和 C 相同派生 A,B 和 C 的对象可分配给 A,但 B 的对象不可分配给 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 '{}'.

因为我们比TS编译器更聪明,我们知道它们是兼容的,而TS不兼容。所以让 TS 相信我们做的是正确的,我们可以做这样的类型断言

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 的类型将是 C

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