React и Typescript: как расширить общие реквизиты компонента?

#reactjs #typescript #generics #react-component

#reactjs #typescript #дженерики #реагирующий компонент

Вопрос:

Представьте гибкий компонент, который принимает a 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> amp; 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> amp; C, Exclude<keyof C, "component" | "otherProp">>' is not assignable to parameter of type 'Attributes amp; C'.
      Type 'Pick<Props<C> amp; C, Exclude<keyof C, "component" | "otherProp">>' is not assignable to type 'C'.
        'Pick<Props<C> amp; 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:

Здесь нам нужно понять некоторые типы утилит и то, как происходит деструктурирование в 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 amp; 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 amp; T) {
    const {a, b, c, ...rest} = param
}
  

В приведенном выше коде предполагаемый тип for rest будет {b: number, c: number} , потому что object i1 содержит только три ключа, и один из них, иначе a говоря, исчерпан. В случае rest2 , TS все еще может определять тип Obj , поскольку ключи из интерфейса I1 исчерпаны. Под исчерпанием я подразумеваю, что они не фиксируются с помощью оператора rest.

Но в случае функции TS не может выполнять этот тип вывода. Я не знаю причины, по которой TS не может этого сделать. Это может быть связано с общими ограничениями.

Что происходит в случае функции, так это то, что тип для rest внутри функции равен Pick<I1 amp; T, Exclude<keyof T, "a" | "b" | "c">> . Exclude исключает ключи a b и c из универсального типа T . Проверьте Исключение здесь. Затем Pick создает новый тип из I1 amp; T с ключами, возвращаемыми Exclude . Поскольку T может быть любого типа, TS не может определить ключи после исключения и, следовательно, выбранные ключи и, следовательно, вновь созданный тип, даже если T ограничен Obj . Вот почему переменная типа rest в функции остается Pick<I1 amp; T, Exclude<keyof T, "a" | "b" | "c">> .

Пожалуйста, обратите внимание, что тип, возвращаемый, Pick является подтипом Obj

Теперь, переходя к вопросу, та же ситуация происходит с componentProps . Выводимый тип будет Pick<Props<C> amp; C, Exclude<keyof C, "otherProp" | "component">> . TS не сможет сузить его. Глядя на подпись React.createElement

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

И вызывая его

 React.createElement(component, componentProps)
  

Предполагаемый тип для P подписи будет C в вашем коде из первого аргумента, т.е. component Потому, что он имеет тип React.ComponentType<C> . Второй аргумент должен быть или undefined или null или C (на данный момент игнорируется Attributes ). Но тип componentProps is Pick<Props<C> amp; C, Exclude<keyof C, "otherProp" | "component">> , который определенно можно присвоить {} , но не C потому, что он является подтипом {} not of C . C также является подтипом {} , но типом выбора и C может быть или не быть совместимым (это то же самое, что — существует класс A; B и C получают A, объекты B и C могут быть присвоены A, но объект B не может быть приписан C). Вот почему ошибка

         'Pick<Props<C> amp; 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> amp; C) => {
  const { otherProp, component, ...componentProps } = props;
  return React.createElement(component, componentProps as unknown as C);
  // ------------------------------------------------^^^^^^^^
};
  

Это определенно правильное утверждение типа, потому что мы знаем, что тип componentProps будет C

Надеюсь, это ответит на ваш вопрос и решит вашу проблему.