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

105 Views Asked by At

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
1

There are 1 best solutions below

0
Filly On BEST ANSWER

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```