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

hen*_*dra 5 generics typescript reactjs react-component

想象一个灵活的组件,它接受 aReact.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);
};
Run Code Online (Sandbox Code Playgroud)

我可以以某种方式直接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);
};
Run Code Online (Sandbox Code Playgroud)

错误:

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 '{}'.
Run Code Online (Sandbox Code Playgroud)

Shi*_*gla 8

这里我们需要了解一些实用程序类型以及 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
}
Run Code Online (Sandbox Code Playgroud)

在上面的代码中, for 的推断类型rest{b: number, c: number}因为 objecti1仅包含三个键,并且其中一个键a已耗尽。在 的情况下rest2,TS 仍然可以推断类型,Obj因为接口中的键I1已用完。我所说的“精疲力尽”是指它们没有使用休息运算符捕获。

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

如果是函数,则rest函数内部的类型是Pick<I1 & T, Exclude<keyof T, "a" | "b" | "c">>。从泛型类型中排除Excludeab。勾选此处的排除。然后,使用 所返回的键创建一个新类型。由于可以是任何类型,因此 TS 无法确定排除后的键,因此无法确定所选取的键以及新创建的类型,即使 T 被限制为。这就是函数中类型变量仍然保留的原因。cTPickI1 & TExcludeTObjrestPick<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>
Run Code Online (Sandbox Code Playgroud)

并称其为

React.createElement(component, componentProps)
Run Code Online (Sandbox Code Playgroud)

P签名中的推断类型将C在您的代码中从第一个参数开始,即component因为它具有 type React.ComponentType<C>。第二个参数应该是undefinedor nullor CAttributes现在忽略)。componentProps但是is的类型Pick<Props<C> & C, Exclude<keyof C, "otherProp" | "component">>,它肯定可以分配给{}但不能分配给C,因为它是{}not of的子类型CC也是 的子类型,{}但选择类型C可能兼容也可能不兼容(这与 - 有一个类 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 '{}'.

Run Code Online (Sandbox Code Playgroud)

由于我们比 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 '{}'.

Run Code Online (Sandbox Code Playgroud)

这绝对是一个正确的类型断言,因为我们知道 of 的类型componentProps将是C

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