Дочерние реквизиты Infer React в интерфейсе TypeScript

Можно ли вывести реквизиты первого дочернего элемента и применить их в TypeScript? Я могу подобрать его близко, но с дженериками он начинает давать сбой, и я не могу определить тип.

Я пытаюсь передать реквизиты компонента из оболочки первому дочернему компоненту с безопасностью типов. Если свойство не существует на объекте, TS должен завершиться неудачей, в противном случае он пройдет.

import React, {
  Children,
  isValidElement,
  cloneElement,
  ReactNode,
} from 'react';

interface WrapperProps<C> {
  children: ReactNode;
  // how can I make typeof Button generic/inferred from the code?
  // firstChildProps: Partial<React.ComponentProps<typeof Button>>; // works with <C> removed
  firstChildProps: Partial<React.ComponentPropsWithoutRef<C>>;
}

const Wrapper: React.FC<WrapperProps<typeof Button>> = ({
  children,
  firstChildProps,
}) => {
  const firstChild = Children.toArray(children)[0];
  if (isValidElement(firstChild)) {
    // Clone the first child element and pass the firstChildProps to it
    return cloneElement(firstChild, { ...firstChildProps });
  } else {
    return <>{children}</>;
  }
};

interface ButtonProps {
  disabled: boolean;
  children: ReactNode;
}

const Button: React.FC<ButtonProps> = ({ disabled, children }) => {
  return <button disabled = {disabled}>{children}</button>;
};

const Example = () => {
  return (
    <>
      {/* Passes type check because disabled exists on Button */}
      <Wrapper firstChildProps = {{ disabled: false }}>
        <Button disabled = {true}>Ok</Button>
      </Wrapper>
      {/* Fails type check because cheese does not exist on Button */}
      <Wrapper firstChildProps = {{ cheese: true }}>
        <Button disabled = {true}>Ok</Button>
      </Wrapper>
    </>
  );
};

Вот почти рабочая TS Playground.

🤔 А знаете ли вы, что...
JavaScript можно использовать для создания анимаций и игр на веб-страницах.


3
80
2

Ответы:

Вам просто нужно добавить общее ограничение к WrapperProps<C> — то же самое, что есть у ComponentPropsWithoutRef<C> — то есть ElementType.

interface WrapperProps<C extends React.ElementType> {
  children: ReactNode;
  firstChildProps: Partial<React.ComponentPropsWithoutRef<C>>;
}

Решено
  1. Вам необходимо ограничить параметр универсального типа C так, чтобы он соответствовал типу утилиты ComponentProps, например. с ElementType, как предложено в ответе СлавыСоболева:
interface WrapperProps<C extends React.ElementType> {
    children: ReactNode;
    firstChildProps: Partial<React.ComponentPropsWithoutRef<C>>;
}
  1. Вы также можете сделать компонент Wrapper универсальным, но вам придется удалить React.FC, что не работает с другими дженериками:
const Wrapper = <C extends React.ElementType>({ children, firstChildProps }: WrapperProps<C>) => {
  // etc.
}

Затем вы можете использовать его для различного контента:

interface OtherProps {
    foo: "bar";
}

const Other: React.FC<OtherProps> = () => null;

{/* Fails type check because cheese does not exist on Button */}
<Wrapper<typeof Button> firstChildProps = {{ cheese: true }}>
    <Button disabled = {true}>Ok</Button>
</Wrapper>

{/* Error: Type 'number' is not assignable to type '"bar"'. */}
<Wrapper<typeof Other> firstChildProps = {{ foo: 0 }}>
    <Other foo = "bar" />
</Wrapper>

Ссылка на игровую площадку