简体   繁体   中英

Default callback return type conflicts with generic type which should be inferred as that return type

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.

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>

Playground Link

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM