0

I have a class that houses my API code for my application:

export class Api {
  ...

  static requestData = async (
    abortController: React.MutableRefObject<AbortController | null>
  ) => {
    // If previous request, cancel
    if (abortController.current) {
      abortController.current.abort();
    }
    // Set new controller for new request
    abortController.current = new AbortController();
    try {
      const response = await instance.get(
        "request/url/string",
        { signal: abortController.current.signal, },
      );
      if (response.success) {
        // Handle successful response
      } else {
        // Handle known error
      }
    } catch (err: unknown) {
      const signal = abortController.current.signal;
      if (abortController.current.signal.aborted) {
        // Handle aborted request
      } else {
        // Handle unknown error
      }
    }
  };

The Axios instance is defined earlier, using axios.create

const instance = axios.create({
  baseURL: "base-url-path",
});

I want to refactor the code that aborts and creates the new AbortController

    // If previous request, cancel
    if (abortController.current) {
      abortController.current.abort();
    }
    // Set new controller for new request
    abortController.current = new AbortController();

into its own function (to be reused elsewhere and reduce the amount of repeated code).

Attempt 1

I tried to move the above code to its own function inside the class:

  public static abortPreviousAndSetUpNewController = (
    abortController: React.MutableRefObject<AbortController | null>
  ) => {
    // If previous request, cancel
    if (abortController.current) {
      abortController.current.abort();
    }
    // Set new controller for new request
    abortController.current = new AbortController();
  };

So that I could do the following:

  static requestData = async (
    abortController: React.MutableRefObject<AbortController | null>
  ) => {
    this.abortPreviousAndSetUpNewController();
    try {
      const response = await instance.get(
        "request/url/string",
        { signal: abortController.current.signal, },
      );
      if (response.success) {
        // Handle successful response
      } else {
        // Handle known error
      }
    } catch (err: unknown) {
      const signal = abortController.current.signal;
      if (abortController.current.signal.aborted) {
        // Handle aborted request
      } else {
        // Handle unknown error
      }
    }
  };

But I get the error: 'abortController.current' is possibly 'null'.ts(18047)

Attempt 2

I also tried to use the is keyword
Example code that made me consider using is:

export const notNullNorUndefined = <T = any>(
  value: T
): value is NonNullable<T> => value !== null && value !== undefined;

If a potentially null/undefined object is passed to this function, if the result is true the compiler accepts that the type of the object passed cannot be null | undefined anymore.
Attempt 2 code:

public static abortPreviousAndSetUpNewController = (
  abortController: React.MutableRefObject<AbortController | null>
): abortController is React.MutableRefObject<AbortController | null>  => {
  // If previous request, cancel
  if (abortController.current) {
    abortController.current.abort();
  }
  // Set new controller for new request
  abortController.current = new AbortController();
}

But this returns syntax errors: A function whose declared type is neither 'undefined', 'void', nor 'any' must return a value.

How do I tell TypeScript that my function (abortPreviousAndSetUpNewController) has mutated abortController.current so that its' type is no longer AbortController | null and only AbortController?

I would prefer not to assert the type where possible

Note, my package versions are:

  • axios - 0.24.0
  • react - 16.14.0
Harrison
  • 1,654
  • 6
  • 11
  • 19
  • the return type should be `asserts abortController is React.MutableRefObject` – Jerryh001 Jun 27 '23 at 03:28
  • @Jerryh001 Agreed, and that's what I tried with "Attempt 2" (taking from the `notNullNorUndefined` function and trying to do the same with `abortPreviousAndSetUpNewController`), but I got syntax errors – Harrison Jun 27 '23 at 08:03
  • @Jerryh001 Thank you! I used your suggestion and another answer to guide me to an acceptable answer – Harrison Jun 27 '23 at 08:35

1 Answers1

0

After Jerryh001's hint and help from another answer (linked below), I was able to get to a solution:

  public static abortPreviousAndSetUpNewController(
    abortController: React.MutableRefObject<AbortController | null>
  ): asserts abortController is React.MutableRefObject<AbortController> {
    // If previous request, cancel
    if (abortController.current) {
      abortController.current.abort();
    }
    // Set new controller for new request
    abortController.current = new AbortController();
  }

Which meant that I could have the desired code:

  static requestData = async (
    abortController: React.MutableRefObject<AbortController | null>
  ) => {
    this.abortPreviousAndSetUpNewController();
    try {
      const response = await instance.get(
        "request/url/string",
        { signal: abortController.current.signal, },
      );
      if (response.success) {
        // Handle successful response
      } else {
        // Handle known error
      }
    } catch (err: unknown) {
      const signal = abortController.current.signal;
      if (abortController.current.signal.aborted) {
        // Handle aborted request
      } else {
        // Handle unknown error
      }
    }
  };

Note:

As per this answer:

keep in mind that TypeScript cannot use arrowFunctions for assertions.

The answer posts a link to this issue which follows on to a suggestion.
Unfortunately, both of these threads are marked as "closed" so for the time-being you can't use arrow functions when using the asserts keyword.

Harrison
  • 1,654
  • 6
  • 11
  • 19