javascript - Hvordan bruke React.FC<props>-typen når barna enten kan være en React-node eller en funksjon


Jeg har denne prøvekomponenten

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;

Min hensikt her er at children-egenskapen til komponenten kan enten være

  • en node, som beskrives som

    Alt som kan gjengis: tall, strenger, elementer eller et array (eller fragment) som inneholder disse typene.

  • en funksjon (eller en «render-prop») som rett og slett henter en verdi fra komponenten og returnerer en annen node

Poenget her er å være eksplisitt, at children enten kan være den ene (node, som er omtrent alt) eller den andre (som helt enkelt er en function)

Problemet

Jeg står overfor følgende problemer med typetesten.

  • hvis jeg lar koden stå som presentert her, får jeg følgende feilmelding på linjen ? props.children(val)

    Dette uttrykket kan ikke kalles. Ikke alle bestanddeler av typen 'Function | ((x: number) => ReactNode) | (string & {}) | (number & {}) | (false & {}) | (true & {}) | ({} & string) | ({} & number) | ({} & false) | ({} & true) | (((x: number) => ReactNode) & string)

Jeg forstår ikke denne feilen.

  • hvis jeg endrer Props-typen til å være
type Props = {
  children: (x: number) => ReactNode;
};

og stole på Reacts egen type PropsWithChildren<P>=P & { children?: ReactNode }; for å håndtere tilfeller der children ikke er en funksjon, så får jeg feilen

(egenskap) children?: PropTypes.Validator<(x: number) => React.ReactNode> Type 'Validator' kan ikke tildeles typen 'Validator<(x: number) => ReactNode>'. Type 'ReactNodeLike' kan ikke tildeles typen '(x: number) => ReactNode'. Type 'string' kan ikke tildeles typen '(x: number) => ReactNode'.ts(2322) Comp.tsx(5, 3): Den forventede typen kommer fra egenskapen 'children' som er erklært her på typen

på linjen children: PropTypes.oneOfType([PropTypes.node, PropTypes.func]).isRequired

Den eneste løsningen er å la Props-typen være

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

og også endre Comp.propTypes til å være children: PropTypes.func.isRequired, som ikke er det jeg ønsker, siden jeg vil være eksplisitt.

Spørsmålet

Hvordan kan jeg holde koden eksplisitt, som vist i starten av dette spørsmålet, og samtidig unngå at typetesting gir meg feil?

CodeSandbox-lenke

Svar #1

kort fortalt :

Typen React.FC er årsaken til feilen ovenfor:

  1. Det inkluderer allerede standard children typet som ReactNode, som blir slått sammen (&) med din egen children-type som er inneholdt i Props.
  2. ReactNode er en ganske bred type som begrenser kompilatorens evne til å avgrense children-unionstypen til en kallbar funksjon i kombinasjon med punkt 1.

En løsning er å utelate FC og bruke en mer spesifikk type enn ReactNode for å dra nytte av typesikkerhet:

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

Mer detaljer

Først av alt, her er de innebygde React-typene:

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.) Du bruker FC<Props> for å typisere Comp. FC inkluderer internt allerede en children-deklarasjon typet som ReactNode, som blir slått sammen med children-definisjonen fra Props:

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

2.) Når man ser på ReactNode-typen, vil man se at typene blir betydelig mer komplekse. ReactNode inkluderer typen {} via ReactFragment, som er supertypen til alt unntatt null og undefined. Jeg kjenner ikke de eksakte beslutningene bak denne typestrukturen, microsoft/TypeScript#21699 antyder historiske og bakoverkompatibilitetsårsaker.

Som en konsekvens er children-typene bredere enn tiltenkt. Dette forårsaker dine opprinnelige feil: typevakt typeof props.children === "function" kan ikke lenger innsnevre typen "muddle" riktig til function.

Løsninger

Utelat React.FC

Til slutt er React.FC bare en funksjonstype med ekstra egenskaper som propTypes, displayName osv., med en meningsfull, bred children-type. Å utelate FC her vil resultere i tryggere, mer forståelige typer for kompilator og IDE-visning. Hvis jeg tar definisjonen din Noe som kan rendres for children, kunne det være:

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

Egendefinert FC-type uten children

Du kan definere din egen FC-versjon, som inneholder alt fra React.FC bortsett fra de brede children-typene:

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 => ...

Lekeplassprøve

Svar #2

Jeg tror at global fagforening kan hjelpe:

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

Relaterte artikler