Búsqueda de sitios web

reactjs - Typescript: errores de tipo al obtener API: "El objeto podría ser 'nulo'."


Estoy refactorizando mi aplicación React a TypeScript y hasta ahora todo bien. Estoy en mi último componente, el que obtiene los datos de la API, y estoy tratando de entender por qué estos errores de tipo no se resuelven. Usé https://jvilk.com/MakeTypes/ para ayudar a estructurar correctamente las interfaces.

El objeto podría ser 'nulo' dos veces para error y stories (líneas comentadas abajo)
El parámetro '___' tiene implícitamente un tipo 'any' dos veces para story y idx (líneas comentadas abajo)

Estoy teniendo problemas con este componente de llamada API. ¿Qué me está faltando?

import React, { FC, ReactElement, useEffect, useState } from "react";
import Story from "./Story";

export interface NewsProps {
  results?: (ResultsEntity)[] | null;
}
export interface ResultsEntity {
  section: string;
  title: string;
  abstract: string;
  url: string;
  multimedia?: (MultimediaEntity)[] | null;
}
export interface MultimediaEntity {
  url: string;
  caption: string;
}

const News: FC<NewsProps> = ({results:ResultsEntity}):ReactElement => {
  const [error, setError] = useState(null);
  const [stories, setStory] = useState(null);

  useEffect(() => {
    const getCurrentPage = () => {
      const url = new URL(window.location.href);
      const page = url.pathname.split("/").pop();
      return page ? page : "home";
    };
    const section = getCurrentPage();
    fetch(
      `https://api.nytimes.com/svc/topstories/v2/${section}.json?api-key=4fzCTy6buRI5xtOkZzqo4FfEkzUVAJdr`
    )
      .then((res) => res.json())
      .then((data) => {
        setTimeout(() => setStory(data), 1500);
      })
      .catch((error) => {
        console.log("Error", error);
        setError(error);
      });
  }, []);

  if (error) {
    return <div>Error: {error.message}</div>;   /// "Object is possibly 'null'" (for error)
  } else 
  if (!stories) {
    return <div>Loading...</div>
  } else {
    return (
      <>
        <ul className="stories">
          {stories.results.map((story, idx) => { // "Object is possibly 'null'" (for stories)
                                                 // Parameter '__' implicitly has an 'any' type (for idx & story)
            return (
              <Story
                key={idx}
                title={story.title}
                abstract={story.abstract}
                img={
                  story &&
                  story.multimedia &&
                  story.multimedia[0] &&
                  story.multimedia[0].url
                    ? story.multimedia[0].url
                    : null
                }
                alt={
                  story &&
                  story.multimedia &&
                  story.multimedia[0] &&
                  story.multimedia[0].caption
                    ? story.multimedia[0].caption
                    : null
                }
                link={story.url}
              />
            );
          })}
        </ul>
      </>
    );
  }
}
export default News;

Respuesta #1

Typescript no entiende el tipo de null

const [error, setError] = useState<string>(null);

Respuesta #2

En tu código, hay dos lugares donde el compilador de TS no conoce los tipos exactos de algunos valores, y te advierte que has intentado hacer cosas (posiblemente) inválidas con ellos.

Agregar | null tampoco ayuda, ya que eso no es la causa del problema, y (a menos que estés usando la bandera --strictNullChecks) null se puede asignar a cualquier tipo por defecto, por lo que no es necesario escribirlo.

Los puntos problemáticos:

  1. error.message

    Aunque compruebes la veracidad de error, el compilador no puede deducir que no puede ser null. Y, por supuesto, acceder a una propiedad en null sería un error, de ahí la advertencia.

    Para resolver este, puedes usar el operador de aserción de no nulo:

     error!.message
    
  2. stories

    Como este valor se establece dinámicamente a partir de una solicitud de red, TS no sabrá su tipo (no puede inferirlo a partir del valor predeterminado, ya que es null). Eso hace que sea any, y las operaciones sobre él también son desconocidas para el compilador (por ejemplo, no sabe que .map() es Array#map(), por lo que tampoco conoce los tipos de los argumentos que pasará a su función de devolución de llamada).

    Para solucionar esto, tendrás que definir el tipo exacto (una interfaz) de stories (y sus propiedades, de forma recursiva). Luego, pasa esa interfaz a useState, para definir el tipo de la variable de estado.

    Sin embargo, como stories también es null a veces, después de definir correctamente su tipo, tendrás que usar la afirmación de no nulo antes de acceder a la propiedad, por la misma razón mencionada arriba.

Para resumir, tu código debería verse así:

import React, { FC, ReactElement, useEffect, useState } from "react";
import Story from "./Story";

export interface NewsProps{
  //Props of the component go here
  //Don't confuse them with state or other variables
}
interface Result {
  results: ResultsEntity[];
}
interface ResultsEntity {
  section: string;
  title: string;
  abstract: string;
  url: string;
  multimedia?: MultimediaEntity[];
}
interface MultimediaEntity {
  url: string;
  caption: string;
}

const News:FC<NewsProps> = ():ReactElement => {
  const [error, setError] = useState<Error | null>(null);
  const [stories, setStory] = useState<Result | null>(null);

  useEffect(() => {
    const getCurrentPage = () => {
      const url = new URL(window.location.href);
      const page = url.pathname.split("/").pop();
      return page ? page : "home";
    };
    const section = getCurrentPage();
    fetch(
      `https://api.nytimes.com/svc/topstories/v2/${section}.json?api-key=4fzCTy6buRI5xtOkZzqo4FfEkzUVAJdr`
    )
      .then((res) => res.json())
      .then((data) => {
        setTimeout(() => setStory(data), 1500);
      })
      .catch((error) => {
        console.log("Error", error);
        setError(error);
      });
  }, []);

  if (error) {
    //                       v-- Non-null assertion (it can't be null, you've checked beforehand)
    return <div>Error: {error!.message}</div>;
  } else 
  if (!stories) {
    return <div>Loading...</div>
  } else {
    return (
      <>
        <ul className="stories">
          //      v-- Non-null assertion (it can't be null, you've checked beforehand)
          {stories!.results.map((story, idx) => {
            return (
              <Story
                key={idx}
                title={story.title}
                abstract={story.abstract}
                img={
                  //              vv-- It's enough to do this (optional chaining, new JS feature). Much more readable. See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining
                  story.multimedia?.[0].url ?? null
                  //   ^^^^^^^^^^^             ^^^^-- Fallback value
                  //   +++++++++++--  Thing that may be null or undefined
                }
                alt={
                  story.multimedia?.[0].caption ?? null //Same as above
                }
                link={story.url}
              />
            );
          })}
        </ul>
      </>
    );
  }
}
export default News;

Véalo en vivo en el TS Playground (no produce errores)