Búsqueda de sitios web

javascript - Cómo usar el tipo React.FC<props> cuando los children pueden ser un nodo de React o una función


Tengo este componente de ejemplo

import React, { FC, ReactNode, useMemo } from "react";
import PropTypes from "prop-types";

type Props = {
  children: ((x: number) => ReactNode) | ReactNode;
};

const Comp: FC<Props> = function Comp(props) {
  const val = useMemo(() => {
    return 1;
  }, []);

  return (
    <div>
      {typeof props.children === "function"
        ? props.children(val)
        : props.children}
    </div>
  );
};

Comp.propTypes = {
  children: PropTypes.oneOfType([PropTypes.node, PropTypes.func]).isRequired
};

export default Comp;

Mi intención aquí es que la propiedad children del componente pueda ser

  • un nodo, que se describe como

    Cualquier cosa que se pueda representar: números, cadenas, elementos o un arreglo (o fragmento) que contenga estos tipos.

  • una función, (o un "render prop") que simplemente obtiene un valor desde dentro del componente y devuelve otro nodo

el punto aquí es ser explícito, que los children pueden ser o uno (node, que es prácticamente todo) o el otro (que es simplemente una function)

El problema

Sin embargo, estoy enfrentando los siguientes problemas con la verificación de tipo.

  • si dejo el código como se presenta aquí, obtengo el siguiente mensaje de error en la línea ? props.children(val)

    Esta expresión no se puede invocar. No todos los constituyentes del tipo 'Function | ((x: number) => ReactNode) | (string & {}) | (number & {}) | (false & {}) | (true & {}) | ({} & string) | ({} & number) | ({} & false) | ({} & true) | (((x: number) => ReactNode) & string)'}

No entiendo este error.

  • si cambio el tipo Props para que sea
type Props = {
  children: (x: number) => ReactNode;
};

y confiar en el propio type PropsWithChildren<P>=P & { children?: ReactNode }; de React para manejar el caso donde children no es una función, entonces obtengo el error

(propiedad) children?: PropTypes.Validator<(x: number) => React.ReactNode> El tipo 'Validator' no se puede asignar al tipo 'Validator<(x: number) => ReactNode>'. El tipo 'ReactNodeLike' no se puede asignar al tipo '(x: number) => ReactNode'. El tipo 'string' no se puede asignar al tipo '(x: number) => ReactNode'.ts(2322) Comp.tsx(5, 3): El tipo esperado proviene de la propiedad 'children' que se declara aquí en el tipo

en la línea children: PropTypes.oneOfType([PropTypes.node, PropTypes.func]).isRequired

La única solución es dejar el tipo Props como

type Props = {
  children: (x: number) => ReactNode;
};

y también cambia el Comp.propTypes a children: PropTypes.func.isRequired, lo cual no es lo que quiero, ya que quiero ser explícito.

La pregunta

¿Cómo puedo mantener el código explícito, tal como se presentó al inicio de esta pregunta, y también evitar que la verificación de tipos me arroje errores?

Enlace de CodeSandbox

Respuesta #1

resumen:

El tipo React.FC es la causa del error anterior:

  1. Ya incluye children por defecto tipados como ReactNode, los cuales se combinan (&) con tu propio tipo de children contenido en Props.
  2. ReactNode es un tipo bastante amplio que limita la capacidad del compilador para reducir el tipo de unión de children a una función invocable en combinación con el punto 1.

Una solución es omitir FC y usar un tipo más específico que ReactNode para aprovechar la seguridad de tipos:

type Renderable = number | string | ReactElement | Renderable[]
type Props = {
  children: ((x: number) => Renderable) | Renderable;
};

Más detalles

Primero que nada, aquí están los tipos integrados de React:

type ReactText = string | number;
type ReactChild = ReactElement | ReactText;

interface ReactNodeArray extends Array<ReactNode> {}
type ReactFragment = {} | ReactNodeArray;
type ReactNode = ReactChild | ReactFragment | ReactPortal | boolean 
  | null | undefined;

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

type PropsWithChildren<P> = P & { children?: ReactNode };

1.) Usas FC<Props> para tipar Comp. FC internamente ya incluye una declaración de children tipada como ReactNode, la cual se fusiona con la definición de children de Props:

type Props = { children: ((x: number) => ReactNode) | ReactNode } & 
  { children?: ReactNode }
// this is how the actual/effective props rather look like

2.) Al observar el tipo ReactNode, verás que los tipos se vuelven considerablemente más complejos. ReactNode incluye el tipo {} a través de ReactFragment, que es el supertipo de todo excepto null y undefined. No conozco las decisiones exactas detrás de esta forma de tipo, microsoft/TypeScript#21699 sugiere razones históricas y de compatibilidad hacia atrás.

Como consecuencia, los tipos children son más amplios de lo previsto. Esto provoca tus errores originales: el guardia de tipo typeof props.children === "function" ya no puede restringir correctamente el tipo "muddle" a function.

Soluciones

Omitir React.FC

Al final, React.FC es solo un tipo de función con propiedades adicionales como propTypes, displayName, etc., con un tipo de children amplio y con opinión. Omitir FC aquí resultará en tipos más seguros y comprensibles para el compilador y la visualización en el IDE. Si tomo tu definición Cualquier cosa que pueda ser renderizada para children, eso podría ser:

import React, { ReactChild } from "react";
// You could keep `ReactNode`, though we can do better with more narrow types
type Renderable = ReactChild | Renderable[]

type Props = {
  children: ((x: number) => Renderable) | Renderable;
};

const Comp = (props: Props) => {...} // leave out `FC` type

Tipo FC personalizado sin children

Podrías definir tu propia versión de FC, que contenga todo de React.FC excepto esos tipos amplios de children:

type FC_NoChildren<P = {}> = { [K in keyof FC<P>]: FC<P>[K] } & // propTypes etc.
{ (props: P, context?: any): ReactElement | null } // changed call signature

const Comp: FC_NoChildren<Props> = props => ...

Muestra de parque infantil

Respuesta #2

Creo que la unión global podría ayudar:

type Props = {
  children: ((x: number) => ReactNode);
} | {
  children: ReactNode;
};

Artículos relacionados