Reputation: 24472
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>'.
Upvotes: 2
Views: 2144
Reputation: 42188
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[]
.
Upvotes: 2