Typescript: API fetch type errors: "Object is possibly 'null'."

Viewed 649

I'm refactoring my React app to Typescript and so far so good. I'm on my last component, the one that fetches the API data and I'm trying to understand why these type errors aren't resolving. I used https://jvilk.com/MakeTypes/ to help scaffold the interfaces correctly.

Object is possibly 'null' twice for error & stories (commented lines below)
Parameter '___' implicitly has an 'any' type. twice for story & idx (commented lines below)

I'm having trouble with this API call component. What am I missing?

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;
2 Answers

In your code, there are two places where the TS compiler doesn't know the exact types of some values, and warns you that you've tried to do (possibly) invalid things with them.

Adding | null doesn't help either, as that's not the cause of the problem, and (unless you are using the --strictNullChecks flag) null is assignable to any type by default, so no need to type it out.

The problematic points:

  1. error.message

    Although you check for the truthyness of error, the compiler can't deduce that it can't be null. And, of course, accessing a property on null would be an error, hence the warning.

    To solve this one, you can use the non-null assertion operator:

     error!.message
    
  2. stories

    As this value is set dynamically from a network request, TS, won't know its type (it can't infer it from the default value, as it's null). That causes it to be any, and operations on it are also unknown to the compiler (e.g. it doesn't know that .map() is Array#map(), so also doesn't know the types of arguments it will pass to its callback).

    To fix this, you'll have to define the exact type (an interface) of stories (and its properties, recursively). Then, pass that interface to useState, to define the type of the state variable.

    However, as stories is also null sometimes, after properly defining its type, you'll have to use the non-null assertion before property access, by the same reasoning as above.

To sum it up, your code should look like this:

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;

See it live in the TS Playground (it produces no errors)

Typescript don't understand the type of null

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