#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 />;
Ответ №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
Я не уверен, что ответил на ваш вопрос, но надеюсь, вы найдете мой ответ полезным
Обновить
Вы также можете переопределить 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.