如果在自定义挂钩中设置了初始值,如何有条件地设置 useState 的通用类型,或从状态中删除类型“未定义”?

How to conditionally set generic type of useState, or remove type `undefined` from state if an initial value is set in custom hook?

我有一个自定义挂钩来帮助对 API 进行异步查询。该挂钩的工作方式类似于常见的 useState 语句,您可以设置一个初始值或将其保留为未定义。在内置 useState 语句的情况下,当指定初始值时,状态的类型不再是未定义的(例如,类型从 (TType | undefined) 更改为 (TType))。 在我的自定义钩子中,我有一个初始状态的可选参数,但是我需要将钩子中的useState的类型指定为(TData | undefined),以防没有传入initiaState。

但是...当传入一个initialState 时,我希望类型只有(TData) 而不是未定义的可能性。否则,我需要在使用钩子的任何地方进行检查,即使设置了初始值并且它永远不会未定义。

有没有办法在我的钩子中有条件地设置 useState 的泛型类型(即,当 (initialState !== undefined) 时,类型只是 (TData),否则就是 (TData | undefined)?

useAsyncState.ts

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

interface PropTypes<TData> {
  /**
   * Promise like async function
   */
  asyncFunc?: () => Promise<TData>;
  /**
   * Initial data
   */
  initialState?: TData,
}

/**
 * A hook to simplify and combine useState/useEffect statements relying on data which is fetched async.
 * 
 * @example
 *   const { execute, loading, data, error } = useAync({
 *     asyncFunc: async () => { return 'data' },
 *     immediate: false,
 *     initialData: 'Hello'
 *   })
 */
const useAsyncState = <TData>({ asyncFunc, initialState }: PropTypes<TData>, dependencies: any[]) => {
  // The type of useState should no longer have undefined as an option when initialState !== undefined
  const [data, setData] = useState<TData | undefined>(initialState);
  const [loading, setLoading] = useState<boolean>(true);
  const [error, setError] = useState<any>(null);
  const isMounted = useRef<boolean>(true);

  const execute = useCallback(async () => {
    if (!asyncFunc) {
      return;
    }

    setLoading(true)

    try {
      const result = await asyncFunc();

      if (!isMounted.current) {
        return;
      }

      setLoading(false);
      setData(result);
    } catch (err) {
      if (!isMounted.current) {
        return;
      }

      setLoading(false);
      setError(err);
      console.log(err);
    }
  }, [asyncFunc])

  useEffect(() => {
    isMounted.current = true;
    execute();

    return () => {
      isMounted.current = false
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [...dependencies])

  return {
    execute,
    loading,
    data,
    error,
    setData,
  }
}

export default useAsyncState;

使用钩子:

  // Currently, the type of data is: number[] | undefined
  const { data } = useAsyncState({
    asyncFunc: async (): Promise<number[]> => {
      return [1, 2, 3]
    },
    initialState: [],
  }, []);

我假设 useState 钩子做的事情与我在这里想做的类似,因为如果使用 useState 设置了初始值,状态类型将不再是未定义的。

本来是想出来something that seemed overcomplicated so I asked Titian Cernicova-Dragomir来看的。他能够简化它(正如我所怀疑的那样)。事实证明,关键是我在构建原始过程的后期所做的事情:在两个重载签名之一中使用 & {initialState?: undefined}undefined 添加到 [=13] 可能的类型中=] 返回对象的成员可能有。

这是结果,带有解释性注释。那里有一个假设:你希望 setData 函数 not 接受 undefined 即使没有 initialState (所以 TData 中有 undefined)。但是,如果您希望 setData 接受 TData(即使它包含 undefined),也有删除它的说明。

import { useCallback, useEffect, useRef, useState, Dispatch, SetStateAction } from "react";

interface PropTypes<TData> {
    /**
     * Promise like async function
     */
    asyncFunc?: () => Promise<TData>;
    /**
     * Initial data
     */
    initialState?: TData,
}

/**
 * The return type of `useAsyncData`.
 */
type UseAsyncDataHookReturn<TData, TSetData extends TData = TData> = {
    // The `TSetData` type is so that we can allow `undefined` in `TData` but not in `TSetData` so
    // `setData` doesn't allow `undefined`. If you don't need that, just do this:
    // 1. Remove `TSetData` above
    // 2. Use `TData` below where `TSetData` is
    // 3. In the "without" overload in the function signature below, remove the second type argument
    //    in `UseAsyncDataHookReturn` (at the end).
    execute: () => Promise<void>;
    loading: boolean;
    data: TData;
    error: any;
    setData: Dispatch<SetStateAction<TSetData>>;
};

/**
 * An overloaded function type for `useAsyncData`.
 */
interface UseAsyncDataHook {
    // The "without" signature adds `& { initialState?: undefined }` to `ProptTypes<TData>` to make
    // `initialState` optional, and adds `| undefined` to the type of the `data` member of the returned object.
    <TData>({ asyncFunc, initialState }: PropTypes<TData> & { initialState?: undefined }, dependencies: any[]): UseAsyncDataHookReturn<TData | undefined, TData>;
    // The "with" signature just uses `ProptTypes<TData>`, so `initialState` is required and is type `TData`,
    // and the type of `data` in the returned object doesn't have `undefined`, it's just `TData`.
    <TData>({ asyncFunc, initialState }: PropTypes<TData>, dependencies: any[]): UseAsyncDataHookReturn<TData>;
}

/**
 * A hook to simplify and combine useState/useEffect statements relying on data which is fetched async.
 * 
 * @example
 *   const { execute, loading, data, error } = useAync({
 *     asyncFunc: async () => { return 'data' },
 *     immediate: false,
 *     initialData: 'Hello'
 *   })
 */
const useAsyncState: UseAsyncDataHook = <TData,>({ asyncFunc, initialState }: PropTypes<TData>, dependencies: any[])  => {
// Only need this comma in .tsx files −−−−−−−−^
    const [data, setData] = useState(initialState);
    const [loading, setLoading] = useState(true); // *** No need for a type argument in most cases
    const [error, setError] = useState<any>(null); // *** But there is here :-)
    const isMounted = useRef(true);
  
    const execute = useCallback(async () => {
        if (!asyncFunc) {
            return;
        }
      
        setLoading(true);
      
        try {
            const result = await asyncFunc();
        
            if (!isMounted.current) {
                return;
            }
        
            setLoading(false);
            setData(result);
        } catch (err) {
          if (!isMounted.current) {
              return;
          }
      
          setLoading(false);
          setError(err);
          console.log(err);
        }
    }, [asyncFunc]);
  
    useEffect(() => {
        isMounted.current = true;
        execute();
      
        return () => {
            isMounted.current = false;
        };
        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [...dependencies]);
  
    return {
        execute,
        loading,
        data,
        error,
        setData,
    };
};

// === Examples

// Array with initial state
const { data, setData } = useAsyncState({
    asyncFunc: async (): Promise<number[]> => {
        return [1, 2, 3]
    },
    initialState: [],
}, []);
console.log(data);
//          ^?
//           type is number[]
console.log(setData);
//          ^?
//          type is Dispatch<SetStateAction<number[]>>

// Array without initial state
const {data: data2, setData: setData2} = useAsyncState({asyncFunc: async () => ([42])}, []);
console.log(data2);
//          ^?
//           type is number[] | undefined
console.log(setData2);
//          ^?
//          type is Dispatch<SetStateAction<number[]>> -- notice that it doesn't allow setting `undefined`

// Object with initial state
const {data: data3, setData: setData3} = useAsyncState({asyncFunc: async () => ({ answer: 42 }), initialState: { answer: 27 }}, []);
console.log(data3);
//          ^?
//           type is {answer: number;}
console.log(setData3);
//          ^?
//          type is Dispatch<SetStateAction<{answer: number;}>>

// Object without initial state
const {data: data4, setData: setData4} = useAsyncState({asyncFunc: async () => ({answer: 42})}, []);
console.log(data4);
//          ^?
//           type is {answer: number} | undefined
console.log(setData4);
//          ^?
//          type is Dispatch<SetStateAction<{answer: number;}>> -- again, notice it doesn't allow `undefined`

// Number with initial state
const {data: data5, setData: setData5} = useAsyncState({asyncFunc: async () => 42, initialState: 27}, []);
console.log(data5);
//          ^?
//           type is number
console.log(setData5);
//          ^?
//          type is Dispatch<SetStateAction<number>>

// Number without initial state
const {data: data6, setData: setData6} = useAsyncState({asyncFunc: async () => 42}, []);
console.log(data6);
//          ^?
//           type is number | undefined
console.log(setData6);
//          ^?
//          type is Dispatch<SetStateAction<number>> -- and again, doesn't allow `undefined`

Playground link