I have a custom hook in React that performs some side-effects for making fetch requests.
At it's core, the hook accepts a fetch function and returns a decorated version to be called by the component. When the decorated function is called, it sets the response to state and returns it as well (similar return structure to useState
).
The passed in fetch function and the decorated function will have exactly the same parameters/types, but the return type will be different.
The issue I'm having is in adding types to both the returned state data and the returned decorated function. I can only seem to get one or the other working without forcing the user of the hook to provide what I believe is duplicate data.
// Dummy useState for dep-free example
function useState<State>(initial: State): [State, (newState: State) => void] {
return [initial, (newState: State) => {}];
}
function useFetch<Data, FetchParams extends any[]>(
fetchFunction: (...args: FetchParams) => Promise<Response>,
initial_value: Data | null = null
): [Data | null, (...args: FetchParams) => Promise<void>] {
const [response, setResponse] = useState<Data | null>(initial_value);
async function wrappedFetch(...args: FetchParams): Promise<void> {
// Side Effects
const res = await fetchFunction(...args);
const result = await res.json();
// Side Effects
if (res.ok) {
setResponse(result);
}
// Side Effects
}
return [
response,
wrappedFetch
];
}
In usage, if you provide the type for the returned state, you must also provide the types of the parameters, which I don't like. Using typescript's Parameters
makes this easy, but it feels unnecessary. This hook will be used hundreds of times in the codebase, and I don't want to force devs to pass in what feels like duplicate data every time.
async function getStuff(value: string): Promise<Response> {
return new Promise((res) => {
res(new Response(value))
})
}
interface SampleData {
value: 'string';
}
const [test, getTest] = useFetch<SampleData, Parameters<typeof getStuff>>(getStuff)
getTest('test'); // Correctly typed, passing nothing or a non-string will error.
If you do not pass any type parameters to useFetch
, it will infer the correct type of the function, but has no way of knowing the type of test
.
Objective
I want to be able to use the hook with the correct type associated with both test
and getTest
, without needing to provide the arguments - like this:
const [test, getTest] = useFetch<SampleData>(getStuff)
// test = SampleData | null
// getTest = (value: string) => Promise<void>
What Else I've tried
- Using
Parameters
inside of the hook:
async function wrappedFetch(...args: Parameters<typeof fetchFunction>): Promise<void> {
But this seems to use the default type provided (any[]
) instead of being based on the actual function passed in.
- Providing a default value to the
FetchParams
so that the developer can pass in theData
type with noFetchParams
required:
function useFetch<Data, FetchParams extends any[] = any[]>(
But providing a default value prevents any type inference from happening making the FetchParams
useless, and we're back to forcing the developer to pass in the arguments every time, just with no error/type safety if they choose not to.