You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

自定义Hook支持任意函数时的TypeScript类型问题修复

Fixing Type Inference for Your Generic Async Operation Hook

Let's tackle the TypeScript type inference issue you're facing with your useAsyncOperation hook. The core problem is that your original generic setup was forcing TypeScript to infer types in the wrong order, leading to data being typed as unknown instead of the expected result type.

Key Issues in Your Original Type Definition

  1. You were defining TResult and TArgs upfront before the TOperation type, which made it harder for TypeScript to automatically extract types from the passed operation function.
  2. The autofire type wasn't properly handling the difference between parameterless operations (where you'd pass true) and operations with arguments (where you'd pass the argument array).

Fixed Hook Implementation

Here's the revised version with correct type inference:

import { useState, useCallback, useEffect } from "react";

// Result type: infers args and result from the operation
type UseAsyncOperationResult<TResult, TArgs extends any[]> = {
  status: "idle" | "pending" | "success" | "error";
  fire: (...args: TArgs) => Promise<void>;
  error?: Error;
  data?: TResult;
};

// Options type: smart autofire typing based on operation args
type UseAsyncOperationOptions<TResult, TArgs extends any[]> = {
  onSuccess?: (result: TResult) => void;
  onError?: (error: Error) => void;
  // Allow boolean for parameterless ops, or args array for ops with parameters
  autofire?: TArgs extends [] ? boolean : TArgs | false;
};

export const useAsyncOperation = <
  TOperation extends (...args: any[]) => Promise<any>
>(
  operation: TOperation,
  options: UseAsyncOperationOptions<
    Awaited<ReturnType<TOperation>>,
    Parameters<TOperation>
  > = {}
): UseAsyncOperationResult<
  Awaited<ReturnType<TOperation>>,
  Parameters<TOperation>
> => {
  const [status, setStatus] = useState<"idle" | "pending" | "success" | "error">(
    "idle"
  );
  const [data, setData] = useState<
    Awaited<ReturnType<TOperation>> | undefined
  >(undefined);
  const [error, setError] = useState<Error | undefined>(undefined);

  const fire = useCallback(
    async (...args: Parameters<TOperation>) => {
      setStatus("pending");
      setError(undefined);
      try {
        const result = await operation(...args);
        setData(result);
        setStatus("success");
        options.onSuccess?.(result);
      } catch (err) {
        const error = err instanceof Error ? err : new Error(String(err));
        setError(error);
        setStatus("error");
        options.onError?.(error);
      }
    },
    [operation, options.onSuccess, options.onError]
  );

  // Handle autofire logic
  useEffect(() => {
    if (options.autofire) {
      if (Array.isArray(options.autofire)) {
        fire(...options.autofire);
      } else {
        // For parameterless operations, call fire with no args
        fire();
      }
    }
  }, [fire, options.autofire]);

  return {
    status,
    fire,
    error,
    data,
  };
};

What Changed & Why

  1. Prioritize Operation Type Inference:

    • We use TOperation as the primary generic parameter, so TypeScript first analyzes the passed operation function.
    • We then extract the result type with Awaited<ReturnType<TOperation>> (handles nested Promises) and parameter types with Parameters<TOperation>, eliminating the need to manually define TResult and TArgs.
  2. Smarter Autofire Typing:

    • The conditional type TArgs extends [] ? boolean : TArgs | false lets you pass autofire: true for parameterless operations, or an argument array for operations that require parameters.

Testing the Fixed Hook

Your example code will now work with correct type inference:

Example 1: Reusable Parameterized Operation

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

// `data` is correctly typed as number | undefined
const { status, data, error } = useAsyncOperation(computeAsync, { autofire: [2, 3] });

Example 2: Inline Operation with Component State

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

// `data` is number | undefined, `fire` is typed as (y: number) => Promise<void>
const { data, fire } = useAsyncOperation(
  (y: number) => computeAsync(x, y),
  { autofire: [10] }
);

Now TypeScript will correctly infer all types, and your hook will work seamlessly with both parameterless and parameterized async operations.

内容的提问来源于stack exchange,提问作者Steve B

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.04 18:15:36