I am working on a React hook and I am running into an issue where I want to have part of the return type be inferred from the return type of a function passed into the hook, but I am not quite getting it working:
const useFetch = <ResponseType>(
fetchInputFactory: () => RequestInfo | URL | [RequestInfo | URL, RequestInit],
deps: DependencyList | undefined,
transformResponse: (response: Response) => Promise<ResponseType> = async (response) => response
):
| { loading: true }
| { loading: false; success: true; response: ResponseType }
| { loading: false; success: false; error: Error }
The error I am getting is:
(parameter) response: Response
Type 'Promise<Response>' is not assignable to type 'Promise<ResponseType>'.
Type 'Response' is not assignable to type 'ResponseType'.
'ResponseType' could be instantiated with an arbitrary type which could be unrelated to 'Response'.ts(2322)
index.ts(6, 22): The expected type comes from the return type of this signature.
CodePudding user response:
The default function you provided for transformResponse
return Promise<Response>
instead of Promise<ResponseType>
You probably can juste cast response and add a default type to ReponseType = Response
const useFetch = <ResponseType = Response>(
fetchInputFactory: () => RequestInfo | URL | [RequestInfo | URL, RequestInit],
deps: DependencyList | undefined,
transformResponse: (response: Response) => Promise<ResponseType> = async (response) => response as ResponseType
):
| { loading: true }
| { loading: false; success: true; response: ResponseType }
| { loading: false; success: false; error: Error }
which from what I guess better describe the default behavior
CodePudding user response:
Unfortunately, a generic type can always be explicitly specified by the consumer code.
Therefore, even though when the 3rd argument transformResponse
is not provided, it defaults to an async identity function, and we would expect TypeScript to infer that ResponseType
is just Response
, this inference could be overridden by the consuming code:
const f = useFetch<number>(someFetchInputFactoryFn, []);
// ^? { loading: true } | { loading: false; success: true; response: number } | { loading: false; success: false; error: Error }
// TS expects `response` to be a number, but it should be a `Response`...
Constraining the generic type is not enough, because a very bad consuming code could always explicitly specify a type that extends Response
, but if it does not provide the corresponding transformResponse
argument, the hook implementation cannot match that arbitrary type.
Hence we want to prevent the possibility to explicitly specify the generic type when transformResponse
is not provided, so that the response type will always be Response
in that case. But if it is provided, then we need the generic type (and if the consuming code still tries to explicitly pass a mismatching type, TS will yell at its transformResponse
value, not at the hook default value).
For this situation, we can use a function overload, so that in one case there is no generics to mess with, and in the other case, the hook return type is associated with the passed transformResponse
callback return type:
// A type to simplify the return expression
type HookReturn<ResponseType> =
| { loading: true }
| { loading: false; success: true; response: ResponseType }
| { loading: false; success: false; error: Error }
// Note: omitting the first 2 arguments, since they do not play a role in the issue
function useFetch2(): HookReturn<Response>;
function useFetch2<ResponseType>(transformResponse: (response: Response) => Promise<ResponseType>): HookReturn<ResponseType>;
function useFetch2(transformResponse = (async (response: Response) => response)) {
return { // Dummy implementation
loading: true
}
}
Let's check its usage:
// Form with no transformResponse, defaults to async identity, and return type to Response
const a = useFetch2() // Okay
// ^? HookReturn<Response>
// Form with no transformResponse, but trying to specify a generic type
const b = useFetch2<number>() // Error: Expected 1 arguments, but got 0.
// ~~~~~~~~~~~~~~~~~~~
// ^? HookReturn<Response>
// Form with provided transformResponse, return type is inferred from the callback return
const c = useFetch2(async () => true) // Okay
// ^? HookReturn<boolean>
// Form with provided transformResponse, explicitly specifying a mismatching generic type
const d = useFetch2<number>(async () => true) // Error: Type 'boolean' is not assignable to type 'number'.
// ~~~~
// ^? HookReturn<number>