Home > Software design >  Typing issue with custom hook that accept any function as parameter, whatever the return type is or
Typing issue with custom hook that accept any function as parameter, whatever the return type is or

Time:01-12

I wrote a custom hook that accept an async function as parameter.

Everything is working as expected with functions that have no parameter :

type UseAsyncOperationResult<TResult> = {
  status: "idle" | "pending" | "success" | "error";
  fire: () => Promise<void>;
  error?: Error;
  data?: TResult;
};

type UseAsyncOperationOptions<TResult> = {
  onSuccess?: (result: TResult) => void;
  one rror?: (error: Error) => void;
  autofire?: boolean;
};

export const useAsyncOperation = <TResult>(
  operation: () => Promise<TResult>,
  options: UseAsyncOperationOptions<TResult> = {}
): UseAsyncOperationResult<TResult> => {
... actual code ...
}

// within a component:

const computeAsync = (x: number, y: number): Promise<number> => {
  const result = x * y;
  console.log(`Computing ${x}*${y}=${result}`);
  return Promise.resolve(result);
};

  const [x, setX] = useState(10);
  const [y, setY] = useState(10);

  const { data, fire } = useAsyncOperation(
    () => {
      return computeAsync(x, y);
    },
    { autofire: true }
  );

The idea is to return an object that has a fire property, which has exactly the same signature than the operation parameter, and a data property that is in this example number (data is ready) or undefined (data is not ready)

this works well when sticking on function that has no parameters.

Working sample here: code sandbox.

I'd like to extend the hook to accept any function with any number or arguments.

I tried to type my function like this:

type UseAsyncOperationResult<TResult, TArgs extends never[]> = {
    status: 'idle' | 'pending' | 'success' | 'error';
    fire: (...args: TArgs) => Promise<void>;
    error: Error | undefined;
    data: TResult | undefined;
};

type UseAsyncOperationOptions<TResult, TArgs extends never[]> = {
    onSuccess?: (result: TResult) => void;
    one rror?: (error: Error) => void;
    autofire?: TArgs;
};

export const useAsyncOperation = <
    TResult,
    TArgs extends never[],
    TOperation extends (...args: TArgs) => Promise<TResult>
>(
    operation: TOperation,
    options: UseAsyncOperationOptions<TResult, TArgs> = {}
): UseAsyncOperationResult<TResult, TArgs> => {
... actual code ...
}

However, the typings are not properly handled.

For example, if I use:

    const computeAsync = useCallback((x: number, y: number): Promise<number> => {
        const result = x * y;
        console.log(`Computing ${x}*${y}=${result}`);
        return Promise.resolve(result);
    }, []);

    const {
        status,
        data,
        error
    } = useAsyncOperation(computeAsync, { autofire: [2, 3] });

The data variable is understood as unknown by typescript, the expected type (should be number)

Another sandbox is available with the extended hook, which is not compiling.

The relevant code in the sandbox is

const computeAsync = (x: number, y: number): Promise<number> => {
  const result = x * y;
  console.log(`Computing ${x}*${y}=${result}`);
  return Promise.resolve(result);
};


// within a component :
  const [x, setX] = useState(10);

  const { data, fire } = useAsyncOperation(
    (y: number) => {
      return computeAsync(x, y);
    },
    { autofire: [10] }
  );

How can I fix my code ?

CodePudding user response:

Here it seems to work as expected :

    type UseAsyncOperationResult<TResult, TArgs extends [...any[]]> = {
        status: "idle" | "pending" | "success" | "error";
        fire: (...args: TArgs) => Promise<void>;
        error: Error | undefined;
        data: TResult | undefined;
    };
    
    type UseAsyncOperationOptions<TResult, TArgs extends [...any[]]> = {
        onSuccess?: (result: TResult) => void;
        one rror?: (error: Error) => void;
        autofire?: TArgs;
    };
    
    export const useAsyncOperation = <
        TResult,
        TArgs extends [...any[]]
    >(
            operation: (...args: TArgs) => Promise<TResult>,
            options: UseAsyncOperationOptions<TResult, TArgs> = {},
        ): UseAsyncOperationResult<TResult, TArgs> => {
        throw new Error("NotImplemented");
    };

The main difference is to use [...any[]] instead of never[].

  • Related