自定义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
- You were defining
TResultandTArgsupfront before theTOperationtype, which made it harder for TypeScript to automatically extract types from the passed operation function. - The
autofiretype wasn't properly handling the difference between parameterless operations (where you'd passtrue) 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
Prioritize Operation Type Inference:
- We use
TOperationas 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 withParameters<TOperation>, eliminating the need to manually defineTResultandTArgs.
- We use
Smarter Autofire Typing:
- The conditional type
TArgs extends [] ? boolean : TArgs | falselets you passautofire: truefor parameterless operations, or an argument array for operations that require parameters.
- The conditional type
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
相关产品推荐
相关产品推荐

