React FunctionComponent 的 TypeScript 类型,它只返回一个 IntrinsicElement

IT技术 reactjs typescript
2021-04-27 14:28:40

我想创建一个 TypeScript 类型来保证 ReactFunctionComponent最终只渲染一个 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';

这确实可以编译。

所以,我希望以下内容适用于FunctionComponent只返回一个的IntrinsicElement

type IntrinsicFC<P = unknown> = FC<P> & ((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> & {
  (props: P): IntrinsicElement | IntrinsicFC;
};

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

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

所以,我的问题是,为什么IntrinsicElementreject string, number, and null, 但IntrinsicFC不是,我该如何解决?

更新:我发现ReactFragments,出于某种原因,成功编译为IntrinsicElements。这可能是因为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;
    }

您可能已经注意到,每个 FUnctionCOMponent 都返回ReactElement<any,any>

这是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 Native 语法是一种更有趣的方式。

考虑下一个例子:

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> & ClassAttributes<HTMLInputElement> | null,
        ...children: ReactNode[]): DetailedReactHTMLElement<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>;
    function createElement<P extends HTMLAttributes<T>, T extends HTMLElement>(
        type: keyof ReactHTML,
        props?: ClassAttributes<T> & P | null,
        ...children: ReactNode[]): DetailedReactHTMLElement<P, T>;
    function createElement<P extends SVGAttributes<T>, T extends SVGElement>(
        type: keyof ReactSVG,
        props?: ClassAttributes<T> & P | null,
        ...children: ReactNode[]): ReactSVGElement;
    function createElement<P extends DOMAttributes<T>, T extends Element>(
        type: string,
        props?: ClassAttributes<T> & P | null,
        ...children: ReactNode[]): DOMElement<P, T>;

    // Custom components

    function createElement<P extends {}>(
        type: FunctionComponent<P>,
        props?: Attributes & P | null,
        ...children: ReactNode[]): FunctionComponentElement<P>;
    function createElement<P extends {}>(
        type: ClassType<P, ClassicComponent<P, ComponentState>, ClassicComponentClass<P>>,
        props?: ClassAttributes<ClassicComponent<P, ComponentState>> & 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> & P | null,
        ...children: ReactNode[]): CElement<P, T>;
    function createElement<P extends {}>(
        type: FunctionComponent<P> | ComponentClass<P> | string,
        props?: Attributes & 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

类型,您感兴趣的 int 是: DetailedReactHTMLElement<HTMLAttributes<HTMLElement>, HTMLElement>

您可以在createElement重载中找到它

随意尝试。

前段时间我写了两篇关于这个话题的文章。你可以在我的博客中找到它们:

打字react儿童

输入react返回类型

不严格相关,但仍然可能对您感兴趣:键入 react props

我不确定我是否已经回答了您的问题,但希望我的回答对您有所帮助

更新

你也可以覆盖FUnctionComponent接口,它部分帮助但仍然允许 React.Fragment:

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 <></>
}