nickhealy
nickhealy

Reputation: 103

Writing wrapper to third party class methods in TS

I am trying to write a wrapper for all my third party API interfaces and SDKs that logs request in standardized, yet customizable way. The way I would like to do this is pass the third party API (usually instatiated with a new API() call) into a wrapper class (APIClient). This client receives an object with certain methods on the third party API mapped to logging functions (that way I can say I can specify when it needs to santize PII, for example). It iterates over this object and redefines on its own this the methods defined on the third party API, calling the logging function after it invokes the third party method. This way the API can have the same interface as the third party API, with the benefit of custom behavior.

I have been struggling for a long time with the typings on this, of course, and feel like I may be close to getting it to work, but I can't quite get it over the line. I was inspried by TS docs section on "Mixins" but I'm not sure that is the way to go.

Some really confusing errors I'm getting:

Type 'Function' provides no match for the signature '(...args: any): any'.

No index signature with a parameter of type 'string' was found on type 'ApiClient<T>'.

(The second one is less confusing, I know that Object.entries gives key values pairs as strings and values, but I am stuck on what else to do)

Does anybody see what might be going wrong here, and how I might fix it? Thank you.

type Constructor = new (...args: any[]) => {};
type Method<T, K extends keyof T> = T[K] extends Function ? T[K] : never;

class ApiClient<T extends Constructor> {
  _api: T;

  constructor(api: T, logConfig: Record<keyof T, () => void>) {
    this._api = api;

    for (const [method, fn] of Object.entries(logConfig)) {
      this[method] = this.createWrappedMethod(method, fn)
     }
  }

  createWrappedMethod<
    N extends keyof InstanceType<T>,
    M extends Method<InstanceType<T>, N>,
  >(name: N, logFn: () => void) {
    return async (...args: Parameters<M>) => {
      try {
        const res = await this._api[name](...args);
        // do logging
      } catch {
        // handle error`
      }
    };
  }
}

Upvotes: 2

Views: 636

Answers (1)

Dennis Kats
Dennis Kats

Reputation: 2299

I'm not sure how to fix the typing issues with your current approach, however, it seems like you are doing something quite similar to what Proxy objects are designed for.

In short, Proxy objects let you redefine certain operations on objects like property accesses, which consequently allows you to wrap and overwrite functions to insert logging and sanitation.

For example, here is an object wrapped in a Proxy that simply prints the result of a method call to the console:

const api = new API();

const proxy = new Proxy(api, {
    get(target, prop) {
        if (typeof target[prop] !== "function") {
            return target[prop];
        }
        return async (...args) => {
            const res = await target[prop](...args);
            console.log(res)
            // do more stuff...
            return res;
        };
    },
});

This also automatically works with TypeScript; the compiler will type and recognize Proxy objects as the type of the original object. In other words, in TypeScript, a Proxy(new API(), {...}) is still an API.

Upvotes: 1

Related Questions