Search code examples
typescripttypescript-genericstypescript-class

In TypeScript, how do I create a generic function which infers the parameters of a function given its class name and function?


Basically, I want to provide a wrapper function for all the RPC calls I am making. This is so that I can log specific information about each RPC call without the use of a middleware. I want to be able to get the parameter of the method which is called by doing rpc[serviceName][method] using TypeScript.

This is my current implementation where the params is not specific enough:

async rpcWrapper<Service extends keyof IRpc>(
    serviceName: Service,
    method: keyof IRpc[Service],
    params: Object,
  ) {
    return rpc[serviceName][method]({ ...params });
  }

I have also tried to do this but have gotten an error:

async rpcWrapper<Service extends keyof IRpc, Method extends keyof IRpc[Service]>(
    serviceName: Service,
    method: Method,
    params: Parameters<Method>, // Type 'Method' does not satisfy the constraint '(...args: any) => any'.
  ) {
    return rpc[serviceName][method]({ ...params });
  }

IRPC interface

    interface IRpc {
        ExampleService: ExampleService;
        ExampleService2: ExampleService2;
        ExampleService3: ExampleService3;
    }

type of an ExampleService

export declare class ExampleService {
  public Login(req: LoginReq): Promise<LoginResp>;
  public Login(ctx: ClientInvokeOptions, req: LoginReq): Promise<LoginResp>;

  public Logout(req: LogoutReq): Promise<CommonResp>;
  public Logout(ctx: ClientInvokeOptions, req: LogoutReq): Promise<CommonResp>;
}

export interface LoginReq {
  username: string;
  email: string;
}

What I want

rpcWrapper("ExampleService", "Login", {  }) 
// Autocomplete tells me that I can fill in username and email

Solution

  • The correct type inference is sometimes hard to accomplish. In your function the Method type is just the key of one method of a service and you can't access the parameters of a PropertyKey. E.g. Parameters<"Login">

    async rpcWrapper<Service extends keyof IRpc, Method extends keyof IRpc[Service]>(
        serviceName: Service,
        method: Method,
        // Method is just the method key of your function
        params: Parameters<Method>, // Type 'Method' does not satisfy the constraint '(...args: any) => any'.
      ) {
        return rpc[serviceName][method]({ ...params });
      }
    

    To get the right Method of your service you have to access this method like this IRPc[Service][Method] //<- but this will result in unknown Therefore you have to check somehow your IRPc[Service][Method]is a valid Function. You can do that by writing a utility type that checks if the provided generic extends any Function type CastFn<T> = T extends AnyFn ? T : never Now you can access the parameters of your method like this Parameters<CastFn<IRPc[Service][Method]>>.

    interface IRpc {
      ExampleService: ExampleService;
      ExampleService2: ExampleService2;
    }
    
    
    
    interface LoginResp {
      message: string
    }
    
    export declare class ExampleService {
      public Login(input: { req: LoginReq, ctx?: ClientInvokeOptions }): Promise<LoginResp>;
      public Logout(input: { req: LogoutReq, ctx?: ClientInvokeOptions }): Promise<CommonResp>;
    }
    
    
    export declare class ExampleService2 {
      public Study(req: StudyReq): Promise<LoginResp>;
    }
    
    export interface StudyReq {
      canStudy: boolean;
    }
    
    export interface LoginReq {
      username: string;
      email: string;
    }
    
    interface LogoutReq {
      username: string;
    }
    
    interface CommonResp { }
    
    interface ClientInvokeOptions { }
    
    declare const rpc: IRpc;
    type AnyFn = (...args: any[]) => any
    
    type CastFn<T> = T extends AnyFn ? T : never
    function rpcWrapper<
      ServiceKey extends keyof IRpc,
      MethodKey extends keyof IRpc[ServiceKey],
      ClassMethod extends AnyFn = CastFn<IRpc[ServiceKey][MethodKey]>
    >(
      serviceName: ServiceKey,
      method: MethodKey,
      ...params: Parameters<ClassMethod>
    ) {
      
      return (rpc[serviceName][method] as ClassMethod)(...params); 
    }
    type X = Parameters<IRpc["ExampleService"]["Login"]>
    // Can put anything
    rpcWrapper("ExampleService", "Login", { a: "a" }) // invalid
    
    // I should be restricted to this
    rpcWrapper("ExampleService", "Login", { req: { username: "haha", email: "[email protected]" } })  //valid```