Typescript: Infer arguments of a callback function passed as a parameter to another function

Viewed 622

I have a simple react hook that accepts one argument, an asynchronous function. It works, however I am not getting any type inference when using the hook. Specifically, I am trying to have typescript infer the type of the arguments of the callback being passed. I have provided the code below and my current usage of the function.

The hook:

import { useCallback, useState } from "react";

type UseAsync<T> = [{ result: T; loading: boolean; error: string }, (...args) => void];

export const useAsync = <T>(asyncFn: (...args) => Promise<T>): UseAsync<T> => {
  const [loading, setLoading] = useState(false);
  const [result, setResult] = useState(null);
  const [error, setError] = useState(null);

  const callback = useCallback(
    async (...args) => {
      try {
        setLoading(true);

        const result = await asyncFn(...args);

        setResult(result);
      } catch (e) {
        setError(e);
      } finally {
        setLoading(false);
      }
    },
    [asyncFn]
  );

  return [{ result, loading, error }, callback];
};

export default useAsync;

My usage:

const [{ result, loading, error }, postOrder] = useAsync((data: INewOrderFormData) =>
    Axios.post("/api/order", data)
);

When I hover over "postOrder" in VSCode i see const postOrder: (...args: any[]) => void where args is defined as any[] when it should be referencing the types of the arguments, if any, of the function passed into the hook.

1 Answers

You can keep track of the argument types if you add an extra generic parameter A extends unknown[] to both UseAsync & useAsync, and type each occurrence of ...args with it.

The result would look something like this (I had to tweak a few null types, possibly because the code wasn't compiled with strictNullChecks):

type UseAsync<T, A extends unknown[]> =
  [{ result: T | null; loading: boolean; error: string }, (...args: A) => void];

export const useAsync = <T, A extends unknown[]>(asyncFn: (...args: A) => Promise<T>): UseAsync<T,A> => {
  const [loading, setLoading] = useState(false);
  const [result, setResult] = useState<T | null>(null);
  const [error, setError] = useState<any>(null);

  const callback = useCallback(
    async (...args: A) => {
      try {
        setLoading(true);

        const result = await asyncFn(...args);

        setResult(result);
      } catch (e) {
        setError(e);
      } finally {
        setLoading(false);
      }
    },
    [asyncFn]
  );

  return [{ result, loading, error }, callback];
};

When applied to a test function, the type of postOrder has the actual argument types:

type INewOrderFormData = {}

const [{ result, loading, error }, postOrder] = 
  useAsync(async (data: INewOrderFormData, arg2: symbol) => 42);

type Test = typeof postOrder
// inferred type: (data: INewOrderFormData, arg2: symbol) => void

TypeScript playground

Related