Тип машинописного текста для компонента функции React, возвращающего ровно один внутренний элемент

#reactjs #typescript

Вопрос:

Я хочу создать тип машинописного текста, который гарантирует, что React FunctionComponent в конечном итоге отобразит ровно один HTML-элемент. То есть это не может быть a string , a number , a ReactFragment , a ReactPortal (если , я полагаю, этот портал не возвращает ровно один HTML-элемент) null , или a boolean .

Насколько я понимаю, тип для всех HTML-элементов является

 type IntrinsicElement = JSX.IntrinsicElements<keyof JSX.IntrinsicElements>;

const anIntrinisicElement: IntrinsicElement = <div />;

// @ts-expect-error
const notAnIntrinsicElement: IntrinsicElement = 'foo';
 

и действительно, это компилируется.

Итак, я ожидал бы, что следующее будет работать для a FunctionComponent , которое возвращает ровно один IntrinsicElement :

 type IntrinsicFC<P = unknown> = FC<P> amp; ((props: P) => IntrinsicElement);

const SuccessFC: IntrinsicFC = () => <div>Goodbye</div>;
const FCWithProps: IntrinsicFC<{ a: number }> = (props: { a: number }) => 
  <div>{props.a}</div>;

// @ts-expect-error
const FragmentFC: IntrinsicFC = () =>
  <>
    <div>Hello</div>
    foo
  </>;

// @ts-expect-error
const NullFC = () => null;

// @ts-expect-error
const TextFC = () => 'foo';
 

Как ни странно, это не компилируется. Ни один из // @ts-expect-error компонентов на самом деле не неисправен.

Если бы это сработало, то я подозреваю, что мой окончательный тип был бы:

 type IntrinsicFC<P = unknown> = FC<P> amp; {
  (props: P): IntrinsicElement | IntrinsicFC;
};

const NestedSuccessFC: IntrinsicFC = () => <SuccessFC />;

// @ts-expect-error
const NestedFragmentFC: IntrinsicFC = () => <FragmentFC />; // erroneously valid
 

Итак, мой вопрос в том, почему IntrinsicElement отклоняется string , number , и null , но IntrinsicFC нет, и как я могу это исправить?

Обновление: Я обнаружил, что ReactFragment s по какой-то причине успешно компилируется как IntrinsicElement s. Это , вероятно , связано с тем, что ReactFragment определяется как {} | ReactNodeList и {} просто «все, что не является нулевым». С другой стороны, ReactFragment не распространяется IntrinsicElement , поэтому я не уверен, почему его можно назначить. Это может быть, а может и не быть корнем проблемы.

 type IsIntrinsicElementAReactFragment =
  IntrinsicElement extends ReactFragment ? true : false; // true

type IsReactFragmentAnIntrinsicElement = 
  ReactFragment extends IntrinsicElement ? true : false; // false

// erroneously valid
// @ts-expect-error
const fragmentIsNotAnIntrinsicElement: IntrinsicElement = <>foo</>;

// correctly invalid
// @ts-expect-error
const notAnIntrinsicElement3: IntrinsicElement = <TextFC />;
 

Игровая площадка TS

Ответ №1:

Это очень интересный вопрос.Некоторое время назад я потратил много времени на изучение встроенных типов react. Это то, что я нашел/понял:

Если вы используете jsx синтаксис, вы никоим образом не сможете вывести некоторые конкретные типы.

Взгляните на определения типов FC

     interface FunctionComponent<P = {}> {
        (props: PropsWithChildren<P>, context?: any): ReactElement<any, any> | null;
        propTypes?: WeakValidationMap<P>;
        contextTypes?: ValidationMap<any>;
        defaultProps?: Partial<P>;
        displayName?: string;
    }

    type VFC<P = {}> = VoidFunctionComponent<P>;

    interface VoidFunctionComponent<P = {}> {
        (props: P, context?: any): ReactElement<any, any> | null;
        propTypes?: WeakValidationMap<P>;
        contextTypes?: ValidationMap<any>;
        defaultProps?: Partial<P>;
        displayName?: string;
    }
 

Как вы, возможно, заметили, возвращается каждый ReactElement<any,any> компонент FUnctionCOmponent .

Вот как Reactlement это выглядит:

     interface ReactElement<P = any, T extends string | JSXElementConstructor<any> = string | JSXElementConstructor<any>> {
        type: T;
        props: P;
        key: Key | null;
    }

 

Нас интересует T общий аргумент типа -. Это расширяет JSXElementConstructor

     type JSXElementConstructor<P> =
        | ((props: P) => ReactElement | null)
        | (new (props: P) => Component<P, any>);
 

JSXElementConstructor это просто рекурсия. Здесь нет ничего полезного.

Собственный синтаксис React намного интереснее.

Рассмотрим следующий пример:

 const foo = React.createElement("a"); // ok

 

Вот где происходит волшебство. Взгляните на createElement шрифты:

     // DOM Elements
    // TODO: generalize this to everything in `keyof ReactHTML`, not just "input"
    function createElement(
        type: "input",
        props?: InputHTMLAttributes<HTMLInputElement> amp; ClassAttributes<HTMLInputElement> | null,
        ...children: ReactNode[]): DetailedReactHTMLElement<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>;
    function createElement<P extends HTMLAttributes<T>, T extends HTMLElement>(
        type: keyof ReactHTML,
        props?: ClassAttributes<T> amp; P | null,
        ...children: ReactNode[]): DetailedReactHTMLElement<P, T>;
    function createElement<P extends SVGAttributes<T>, T extends SVGElement>(
        type: keyof ReactSVG,
        props?: ClassAttributes<T> amp; P | null,
        ...children: ReactNode[]): ReactSVGElement;
    function createElement<P extends DOMAttributes<T>, T extends Element>(
        type: string,
        props?: ClassAttributes<T> amp; P | null,
        ...children: ReactNode[]): DOMElement<P, T>;

    // Custom components

    function createElement<P extends {}>(
        type: FunctionComponent<P>,
        props?: Attributes amp; P | null,
        ...children: ReactNode[]): FunctionComponentElement<P>;
    function createElement<P extends {}>(
        type: ClassType<P, ClassicComponent<P, ComponentState>, ClassicComponentClass<P>>,
        props?: ClassAttributes<ClassicComponent<P, ComponentState>> amp; P | null,
        ...children: ReactNode[]): CElement<P, ClassicComponent<P, ComponentState>>;
    function createElement<P extends {}, T extends Component<P, ComponentState>, C extends ComponentClass<P>>(
        type: ClassType<P, T, C>,
        props?: ClassAttributes<T> amp; P | null,
        ...children: ReactNode[]): CElement<P, T>;
    function createElement<P extends {}>(
        type: FunctionComponent<P> | ComponentClass<P> | string,
        props?: Attributes amp; P | null,
        ...children: ReactNode[]): ReactElement<P>;
 

Эта функция знает все. Ты хочешь IntrinsicElements ? Нет проблем!

 const createElement = <T extends keyof JSX.IntrinsicElements>(elem: T) =>
  React.createElement(elem);

const test = createElement("a"); // ok
const test_ = createElement(null); // error
 

Тип, вас интересует, что такое : DetailedReactHTMLElement<HTMLAttributes<HTMLElement>, HTMLElement>

Вы можете найти его внутри createElement перегрузок.

Не стесняйтесь экспериментировать.

Некоторое время назад я написал две статьи на эту тему. Вы можете найти их в моем блоге:

Ввод текста реагируют дети

Ввод возвращаемого типа react

Не строго связаны, но все равно могут быть вам интересны: Ввод реквизита react

Я не уверен, что ответил на ваш вопрос, но надеюсь, вы найдете мой ответ полезным

Обновить

Вы также можете переопределить FUnctionComponent интерфейс, он частично помогает, но все равно реагирует.Фрагмент:

 interface FunctionComponent<P = {}> {
  (props: PropsWithChildren<P>, context?: any): React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>

}

// ok
const Foo: FunctionComponent<{ label: string }> = (props) => {
  return <div></div>
}

// error
const Bar: FunctionComponent<{ label: string }> = (props) => {
  return 42
}

// error
const Baz: FunctionComponent<{ label: string }> = (props) => {
  return null
}

// error
const Bat: FunctionComponent<{ label: string }> = (props) => {
  return 'str'
}

const x = React.Fragment
// no error
const Fragment: FunctionComponent<{ label: string }> = (props) => {
  return <></>
}
 

Комментарии:

1. Да, на самом деле мне это не помогает. createElement() делает вывод на его T основе elem . Кроме того, я попытался объявить константу типа DetailedReactHTMLElement<HTMLAttributes<HTMLElement>, HTMLElement> и установить ее на <div>hello</div> значение, но это не удалось. Он даже не сработал , когда я перешел HTMLElement на HTMLDivElement него, что меня смущает.

2. Он будет работать только с React.createElement . Он не работает с jsx синтаксисом

3. Мне все равно, использую ли я пространство имен JSX или нет , и, используя одно из определений React.createElement() , должен быть способ создания типа, хотя для этого может потребоваться вручную ввести параметр универсального типа, что было бы неприятно.

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

5. потому что суть проблемы в фрагментах. Все остальное работает с моим подходом JSX.