I am developing a hook in which I can pass a function that makes a web request and it returns isLoading
, data
and error
.
const [isLoading, data, error] = useApi(getMovie, idMovie, someAction);
basically I have a hook (useApi
) that receives 3 parameters:
I use it like this:
const idMovie = { _id: "3" };
// callback function
const someAction = (data: unknown) => {
// do something
return data;
};
const [isLoading, data, error] = useApi(getMovie, idMovie, someAction);
useApi.tsx
import { AxiosPromise, AxiosResponse } from 'axios';
import { useState, useEffect } from 'react';
const useApi = (
apiFunction: (params: unknown) => AxiosPromise,
params = {},
callback: (data: unknown) => void
): [boolean, unknown, null | string] => {
const [data, setData] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<null | string>(null);
useEffect(() => {
apiFunction(params)
.then(({ data }: AxiosResponse) => {
setData(data);
setIsLoading(false);
if (callback) {
callback(data);
}
})
.catch(() => {
setError('Something went wrong');
setIsLoading(false);
});
}, []); //apiFunction, params, callback]
return [isLoading, data, error];
};
export default useApi;
getMovie
corresponds to a function that solves a web request
import axios from "axios";
type getMovieParams = { _id: string };
const BASE_URL = "https://jsonplaceholder.typicode.com/todos/";
const getMovie = (params: getMovieParams): Promise<unknown> => {
if (params) {
const { _id } = params;
if (_id) {
const url = `${BASE_URL}/${_id}`;
return axios.get(url);
}
}
throw new Error("Must provide a query");
};
export default getMovie;
the code that calls this hook would look like this:
import "./styles.css";
import useApi from "./useApi";
import getMovie from "./api";
interface Movie {
userId: number;
id: number;
title: string;
completed: boolean;
}
export default function App() {
const idMovie = { _id: "3" };
// callback function
const someAction = (data: unknown) => {
// do something
return data;
};
const [isLoading, data, error] = useApi(getMovie, idMovie, someAction);
const dataResponse = error ? [] : data; //type Movie
console.log(dataResponse);
if (isLoading) {
return <div>Loading...</div>;
}
return (
<div>
{error && <div>{error}</div>}
-- Get Data Movie 1--
<p>{dataResponse.title}</p>
</div>
);
}
I am getting an typing error on this line:
<p>{dataResponse.title}</p>
this is because:
const dataResponse = error ? [] : data;
data
is of type unknown
, this is because it could be any type of data
, but I want to specify in this case that the data type is 'Movie
', the idea is to reuse this hook in another component and be able to say the type of data that will be obtained when this hook returns data
.
this is my live code:
https://codesandbox.io/s/vigilant-swartz-iozn4
The reason for the creation of this question is to ask for your kind help to solve the typescript problems that are marked with a red line. (file app.tsx)
How can fix it? thanks
To solve the "Type 'unknown' is not assignable to type" TypeScript error, use a type assertion or a type guard to verify that the two values have compatible types before the assignment. The error is caused when a value of type unknown is assigned to a value that expects a different type.
unknown is the type-safe counterpart of any . Anything is assignable to unknown , but unknown isn't assignable to anything but itself and any without a type assertion or a control flow based narrowing. Likewise, no operations are permitted on an unknown without first asserting or narrowing to a more specific type.
The error "Argument of type 'unknown' is not assignable to parameter of type" occurs when we try to pass an argument of type unknown to a function that expects a different type. To solve the error, use a type assertion or a type guard when calling the function.
The difference between unknown and any is described as: Much like any , any value is assignable to unknown ; however, unlike any , you cannot access any properties on values with the type unknown , nor can you call/construct them. Furthermore, values of type unknown can only be assigned to unknown or any .
It is possible to use generics to capture types related to the args you're passing to a function. For instance, in the screenshot you have, TS complaints about the params, we can declare a generic to capture the params type
function useApi<Params>(
apiFunction: (params: Params) => AxiosPromise,
params: Params,
callback: (data: unknown) => void
)
with this, our second hook argument will be typed as whatever type the apiFunction
asks for in its arg.
we do the same to type the data we return to the callback
function useApi<Params, Return>(
apiFunction: (params: Params) => Promise<AxiosResponse<Return>>,
params: Params,
callback: (data: Return) => void
)
I got the return type of the apiFunction
by inspecting the types for the axios.get
call
get<T = any, R = AxiosResponse<T>>(url: string, config?: AxiosRequestConfig): Promise<R>;
as you can see, in the end it returns a Promise<AxiosResponse<T>>
. We intercept that T
with our generic typing and name it Return
now we can use these typings in the return types and body of the hook as well
): [boolean, Return | null, null | string] {
const [data, setData] = useState<Return | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<null | string>(null);
useEffect(() => {
apiFunction(params)
.then(({ data }) => {
setData(data);
setIsLoading(false);
if (callback) {
callback(data);
}
})
.catch(() => {
setError("Something went wrong");
setIsLoading(false);
});
}, []); //apiFunction, params, callback]
return [isLoading, data, error];
}
as your Movie model is specific for the getMovie call, you can move that interface there and type the axios.get
call
interface Movie {
userId: number;
id: number;
title: string;
completed: boolean;
}
const getMovie = (params: getMovieParams) => {
if (params) {
const { _id } = params;
if (_id) {
const url = `${BASE_URL}/${_id}`;
return axios.get<Movie>(url);
}
}
throw new Error("Must provide a query");
};
with all these additions you'll observe some warnings on the code when you're using the hook that will force you to implement the rendering taking into account all possible values, I rewrote it like this
...
const [isLoading, dataResponse, error] = useApi(
getMovie,
idMovie,
someAction
);
if (isLoading) {
return <div>Loading...</div>;
}
if (error) return <div>error</div>;
return <div>{dataResponse?.title}</div>;
...
https://codesandbox.io/s/generic-hook-arg-types-s8vtt
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With