node.jstypescripttypescript-genericsfunction.prototype

Typescript: Typing return for a "retry function" using .call


If I'm passing a typed function to Function.prototype.call, is there a way to infer the return type from the passed function?

Below I would expect response to be typed based on someTypedFunction return type, but instead is unknown. Are there generics for .call? Or would should I type it as any and set the type of the response?

function getNewToken(): number {
   return Math.floor(Math.random()*1000);
}

//Wrapper - Outside Parent Class
async function retryOnce<R>(this: any, fn: () => R): Promise<R> {
   let isRetry = false;
   const execute = async () => {
      try{
         return await fn();
      }catch(error: any){
         if(!isRetry){
           isRetry = true;
           this.token = getNewToken();
           return execute();
         }else{
          throw Error();
         }
      }
  }
  return execute();
}

function someTypedFunction(option: string, token: number): Promise<string>{
  return new Promise((resolve, reject) => {
    if(token){
       return resolve(option);
    }
    return reject("No token");
  });
}

class Parent {
   token;
   constructor(token: number){
     this.token = token;
   }
   async someMethod(){
      //Usage - Inside Parent Class
      const bar = () => someTypedFunction("variable", this.token);
      const response = await retryOnce.call(this, bar);
      return response;
   }
}


Solution

  • call is inferring its generic types from retryOnce's which isn't inferring R because it's not being directly called with bar.

    One thing you can do is have retyOnce return a function with the desired functionality, that way you can call retryOnce(bar), and the returned function will have the correct return type, which call can then infer correctly.

    function getNewToken(): number {
      return Math.floor(Math.random() * 1000);
    }
    
    //Wrapper - Outside Parent Class
    function retryOnce<R>(fn: () => R): (this: Parent) => Promise<R> { // Return a new function
      return async function () {
        let isRetry = false;
        const execute = async () => {
          try {
            return await fn();
          } catch (error: any) {
            if (!isRetry) {
              isRetry = true;
              this.token = getNewToken();
              return execute();
            } else {
              throw Error();
            }
          }
        }
        return execute();
      }
    }
    
    function someTypedFunction(option: string, token: number): Promise<string> {
      return new Promise((resolve, reject) => {
        if (token) {
          return resolve(option);
        }
        return reject("No token");
      });
    }
    
    class Parent {
      token;
      constructor(token: number) {
        this.token = token;
      }
      async someMethod() {
        //Usage - Inside Parent Class
        const bar = () => someTypedFunction("variable", this.token);
        const response = await retryOnce(bar).call(this);
             // ^?const response: string
        return response;
      }
    }
    

    Playground