通用 Typescript Type + React hook [英] Generic Typescript Type + React hook
问题描述
我有以下 http 钩子:
export const useHttp = (initUrl: string, initData: T) =>{const [url, setUrl] = useState(initUrl);const [状态,调度] = useReducer(fetchReducer, {isLoading: 假,错误: '',数据:初始化数据});useEffect(() => {让取消请求 = 假;const fetchData = async (cancelRequest: boolean = false) =>{如果(!url)返回;dispatch({ type: 'API_REQUEST'});尝试 {const responsePromise: AxiosPromise= axios(url);常量响应 = 等待响应承诺;如果(取消请求)返回;dispatch({ type: 'API_SUCCESS', payload: response.data });}赶上(e){console.log(出现错误", e);dispatch({ type: 'API_ERROR', payload: e.message });}};fetchData(cancelRequest);返回 () =>{取消请求 = 真;}}, [网址]);const executeFetch = (url: string) =>{设置网址(网址);};返回 { ...状态,executeFetch}};
减速器:
const fetchReducer = (state: IState, action: TAction): IState=>{开关(动作.类型){案例API_REQUEST":返回 {...状态,isLoading: 真};案例'API_SUCCESS':返回 {...状态,数据:action.payload,isLoading: 假,错误: ''};案例'API_ERROR':console.error(`触发:${API_ERROR},消息:${action.payload}`);返回 {...状态,错误:action.payload,isLoading: 假,};默认:throw Error('无效动作');}};
动作:
导出接口IApiSuccess{类型:types.ApiSuccess,有效载荷:T;}导出类型 TAction= IApiRequest |IApiSuccess<T>|IApi错误;
像这样使用:
const { data, error, isLoading, executeFetch } = useHttp('news', []);返回 (<><div className={classes.articleListHeader}><h1>文章列表</h1><small className={classes.headerSubtitle}>{data.length} 文章</small>
<ul>{data.map(article => <Article article={article}/>)}</>)
我的 TS 对我大喊大叫,因为我使用了 data
变量:Object 的类型是unknown".TS2571
我确实指定了 useHttp 的类型,即 IArtlce[].知道我错过了什么吗?
更新:我试图为我的减速器添加返回类型:
接口 HttpReducer;扩展 IState<T>{executeFetch: (url: string) =>空白}export const useHttp = (initUrl: string, initData: T): HttpReducer=>{
但我明白了:
Type '{ executeFetch: (url: string) =>空白;错误:字符串;isLoading:布尔值;数据:未知;}' 不可分配给类型 'HttpReducer'.
我能够重现您的错误.您期望 useReducer
钩子能够根据初始状态的类型推断状态类型,但它只是推断 IState
.
useReducer
的类型 被定义 使得泛型参数是reducer的类型.状态的类型是从具有 ReducerState
实用程序类型的 reducer 中推断出来的.它不期待通用的减速器并且不能很好地使用它.
钩子的T
和reducer的T
之间没有关系,而不是状态.fetchReducer
是一个通用函数,这意味着它可以接受 any IState
并返回相同类型的 IState
.我们可以使用这个函数来处理我们钩子的IState
,但是为了推断状态的类型,我们需要说我们的函数将只接受并返回 IState
.
您需要将 useReducer
上的泛型设置为:
const [state, dispatch] = useReducer<(state: IState, action: TAction) =>IState<T>(...
从表面上看,这与目前的推断非常相似,即:
const [state, dispatch] = useReducer<(state: IState, action: TAction) =>IState<T>(...
但差异至关重要.当前描述了一个通用函数,而修复描述了一个函数,它只采用一种类型的 T
—— 那种类型的 useHttp
钩子.这是误导,因为您同时使用 T
.或许我们重命名一个更容易看出来.
我们有一个通用函数:
export const useHttp = (initUrl: string, initData: Data) =>{const [url, setUrl] = useState(initUrl);const [state, dispatch] = useReducer<(state: IState, action: TAction) =>IState<T>>(fetchReducer, {
我们需要该函数的特定用例:
export const useHttp = (initUrl: string, initData: Data) =>{const [url, setUrl] = useState(initUrl);const [state, dispatch] = useReducer<(state: IState, action: TAction) =>IState<Data>>(fetchReducer, {
当我们知道我们的reducer state类型是IState
,那么我们就知道data
的类型是Data
.
现在调用 useHttp
会给你一个 data
类型为 IArticle[]
的变量.
I have the following http hook:
export const useHttp = <T,>(initUrl: string, initData: T) => {
const [url, setUrl] = useState(initUrl);
const [state, dispatch] = useReducer(fetchReducer, {
isLoading: false,
error: '',
data: initData
});
useEffect(() => {
let cancelRequest = false;
const fetchData = async (cancelRequest: boolean = false) => {
if (!url) return;
dispatch({ type: 'API_REQUEST'});
try {
const responsePromise: AxiosPromise<T> = axios(url);
const response = await responsePromise;
if (cancelRequest) return;
dispatch({ type: 'API_SUCCESS', payload: response.data });
} catch (e) {
console.log("Got error", e);
dispatch({ type: 'API_ERROR', payload: e.message });
}
};
fetchData(cancelRequest);
return () => {
cancelRequest = true;
}
}, [url]);
const executeFetch = (url: string) => {
setUrl(url);
};
return { ...state, executeFetch}
};
Reducer:
const fetchReducer = <T,>(state: IState<T>, action: TAction<T>): IState<T> => {
switch (action.type) {
case 'API_REQUEST':
return {
...state,
isLoading: true
};
case 'API_SUCCESS':
return {
...state,
data: action.payload,
isLoading: false,
error: ''
};
case 'API_ERROR':
console.error(`Triggered: ${API_ERROR}, message: ${action.payload}`);
return {
...state,
error: action.payload,
isLoading: false,
};
default:
throw Error('Invalid action');
}
};
actions:
export interface IApiSuccess<T> {
type: types.ApiSuccess,
payload: T;
}
export type TAction<T> = IApiRequest | IApiSuccess<T> | IApiError;
Using like this:
const { data, error, isLoading, executeFetch } = useHttp<IArticle[]>('news', []);
return (
<>
<div className={classes.articleListHeader}>
<h1>Article List</h1>
<small className={classes.headerSubtitle}>{data.length} Articles</small>
</div>
<ul>
{data.map(article => <Article article={article}/>)}
</ul>
</>
)
My TS yell at me because I'm using the data
variable: Object is of type 'unknown'. TS2571
I did specify the type of the useHttp which is IArtlce[]. Any idea what i'm missing?
Update: I tried to add return type for my reducer:
interface HttpReducer<T> extends IState<T> {
executeFetch: (url: string) => void
}
export const useHttp = <T,>(initUrl: string, initData: T): HttpReducer<T> => {
but I get:
Type '{ executeFetch: (url: string) => void; error: string; isLoading: boolean; data: unknown; }' is not assignable to type 'HttpReducer<T>'.
I was able to reproduce your error. You are expecting that the useReducer
hook will be able to infer the state type based on the type of the initial state, but it's just inferring IState<unknown>
.
The types for useReducer
are defined such that the generic argument is the type of the reducer. The type for the state is inferred from the reducer with the ReducerState
utility type. It's not expecting a generic reducer and doesn't work well with it.
There is no relation between the T
of the hook and the T
of the reducer rather than the state. fetchReducer
is a generic function which means that it can take any IState
and return an IState
of the same type. We can use this function to process the IState<T>
of our hook, but in order to infer the type of the state we need to say that our function will only accept and return IState<T>
.
You need to set the generic on you useReducer
to this:
const [state, dispatch] = useReducer<(state: IState<T>, action: TAction<T>) => IState<T>>( ...
On the surface this looks very similar to what's being inferred right now, which is:
const [state, dispatch] = useReducer<<T,>(state: IState<T>, action: TAction<T>) => IState<T>>(...
But the difference is critically important. The current describes a generic function while the fix describes a function that only takes one type of T
-- that of the useHttp
hook. It's misleading because you are using T
for both. Perhaps it is easier to see if we rename one.
We had a generic function:
export const useHttp = <Data,>(initUrl: string, initData: Data) => {
const [url, setUrl] = useState(initUrl);
const [state, dispatch] = useReducer<<T,>(state: IState<T>, action: TAction<T>) => IState<T>>(fetchReducer, {
We need a specific use case of that function:
export const useHttp = <Data,>(initUrl: string, initData: Data) => {
const [url, setUrl] = useState(initUrl);
const [state, dispatch] = useReducer<(state: IState<Data>, action: TAction<Data>) => IState<Data>>(fetchReducer, {
When we know that our reducer state type is IState<Data>
, then we know that the type of data
is Data
.
Now calling useHttp<IArticle[]>()
gives you a data
variable with type IArticle[]
.
这篇关于通用 Typescript Type + React hook的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!