typescriptfunctiongenerics

How to define generic instance arrow function for generic function type


I need to define a type for a function signature to be used in more than one function definition. The type needs to be generic and receive a generic type which is going to be used to cast the function result to it.

working examples:

async function callEndpoint1<ReturnT>(
  url: string,
  init?: RequestInit
  ):Promise<ReturnT> {
 // assume the object string is return from an http request
   return JSON.parse("{}") as ReturnT;
}

async function callEndpoint2<ReturnT>(
  url: string,
  init?: RequestInit
  ):Promise<ReturnT> {
 // assume the object string is return from an http request
   return JSON.parse("{}") as ReturnT;
}

const x1 = callEndpoint1<{prop:string}>("a-url");
const x2 = callEndpoint2<{prop:string}>("a-url");

This is what I am trying to achieve but I cannot:

type CallEndpointFunction = <ReturnT>(
    url: string,
    init?: RequestInit
) => Promise<ReturnT>;

const callEndpoint3: CallEndpointFunction = async (url, init) => {
    return JSON.parse("{}") as ReturnT; // error!
    //                         ~~~~~~~
    // Cannot find name 'ReturnT'.
}
const x3 = callEndpoint3<{ prop: string }>("a-url");

Update Though I am still curious to know if there is an answer for what I was trying to achieve above in TS, I am not going to use the answer as it seems it is not best practice for a function to receive a generic type parameter to just blindly cast the result to.


Solution

  • See microsoft/TypeScript#40287 for an authoritative answer.

    If you want access to the generic type parameter name in a generic function implementation, you'll need to explicitly declare the generic type parameter and also annotate the function parameters explicitly:

    const callEndpoint: CallEndpointFunction =
        async <ReturnT,>(url: string, init?: RequestInit) => {
            return JSON.parse("{}") as ReturnT
        };
    

    Note that the names of the type parameter and the function parameters are arbitrary; they are not required to be the same as the names in the CallEndpointFunction type definition. This is the same thing as above:

    const callEndpoint: CallEndpointFunction =
        async <X,>(u: string, i?: RequestInit) => {
            return JSON.parse("{}") as X
        };
    

    If you decide not to annotate the url and init function parameters because you want to use contextual typing to have TypeScript infer their types for you, then you unfortunately cannot still declare the generic type parameter. If you declare the type parameter, then contextual typing just doesn't happen, and the parameter types implicitly fall back to any:

    const callEndpoint: CallEndpointFunction =
        async <ReturnT,>(url, init) => { // error!
            //           ~~~  ~~~~
            // Parameter implicitly has an 'any' type.
            return JSON.parse("{}") as ReturnT
        };
    

    I could imagine someone filing a feature request for that to work with contextual typing, but I doubt it would have much community demand.

    On the other hand if you leave off the generic type parameter declaration, contextual typing works, and the function is still generic. But now the type parameter is anonymous; you have no way to refer to it by name, as you've seen:

    const callEndpoint: CallEndpointFunction = async (url, init) => {
        return JSON.parse("{}") as ReturnT; // error!
        //                         ~~~~~~~
        // Cannot find name 'ReturnT'.
    }
    

    So you should just explicitly annotate the function call signature if you want to do this. For your particular example it doesn't really matter because JSON.parse() returns any in TypeScript, and any is assignable to the anonymous type parameter:

    const callEndpoint7: CallEndpointFunction =
        async (url, init) => {
            return JSON.parse("{}"); // okay because any
        }
    

    At this point, you're more or less out of options. The type parameter is only mentioned in the function's return type, so you have no value inside the function implementation whose type can be used to recover the anonymous type parameter. It's not great for the type parameter to only be referenced once like this; it violates the golden rule of generics that type parameters should be mentioned in at least two places, and in this case the function just claims to be able to return a value of some type that has nothing to do with any of the function arguments. Which is impossible to do safely (callEndpoint<string>("") and callEndpoint<number>("") both compile to callEndpoint(""), and presumably the actual function cannot possibly return a string in one case and a number in the other.)

    If the function referred to the type parameter multiple times, as in

    type SomeOtherFunc = <ReturnT>(
        x: ReturnT[] // <-- reference here
    ) => Promise<ReturnT>; // <-- reference here
    

    then you could use contextual typing and have access to values whose types include the anonymous type parameter:

    const func: SomeOtherFunc = async (x) => {
        const ret = x[0];
        //    ^? const ret: ReturnT (this type is anonymous)
        return ret;
    }
    

    This is the most common way functions with anonymous generic types get implemented, without needing to mention the type explicitly at all (and that's why I doubt the aforementioned feature request would be popular). But, if necessary, you could use the parameter to give a name to the anonymous type parameter:

    const func: SomeOtherFunc = async (x) => {
        type MyReturnT = typeof x[number];
        // type MyReturnT = ReturnT (anonymous)
        return JSON.parse("{}") as MyReturnT; // okay
    }
    

    That's a reasonable workaround for your problem, but again, only in golden-rule situations where the type parameter can be recovered from the function parameter types. Your code violates that rule (which is why, as you said, it's not necessarily a best practice) so your only choice is to manually annotate everything, if you actually need to refer to the generic type parameter name.

    Playground link to code