如果在自定义挂钩中设置了初始值,如何有条件地设置 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`
我有一个自定义挂钩来帮助对 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`