Creating a wrapper for methods of a third-party class in TypeScript

I have been working on creating a wrapper for all my third party API interfaces and SDKs that logs requests in a standardized yet customizable way. My approach involves passing the third party API (typically instantiated with a new API() call) into a wrapper class (APIClient). This client receives an object with specific methods from the third party API mapped to logging functions, allowing me to specify actions such as sanitizing Personally Identifiable Information (PII). The process involves iterating over this object and redefining the methods defined on the third party API within the wrapper, triggering the logging function after invoking the third party method. This setup enables the API to maintain the same interface as the original API with added custom behavior.

Despite making progress towards getting this system to work, I have encountered challenges with typings that are proving difficult to overcome. Although inspired by TypeScript documentation on "Mixins," I am uncertain if that is the correct approach in this scenario.

Some of the error messages I'm facing are quite perplexing:

Type 'Function' does not match the signature '(...args: any): any'.

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

(The second error is somewhat clearer to me, as I understand that Object.entries presents key-value pairs as strings and values. However, I'm unsure about the next step.)

If anyone can spot what might be causing these issues and offer suggestions on how to resolve them effectively, I would greatly appreciate 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);
        // perform logging
      } catch {
        // handle errors`
      }
    };
  }
}

Answer №1

It appears that your current approach to fixing typing issues may be similar to the concept of Proxy objects in JavaScript.

Proxy objects offer the ability to redefine certain operations on objects such as property accesses, allowing for functions to be wrapped and modified for logging and sanitation purposes.

As an illustration, consider an object enclosed within a Proxy that simply logs 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)
            // additional actions...
            return res;
        };
    },
});

This feature seamlessly integrates with TypeScript, where the compiler is capable of recognizing and typing Proxy objects as the original object type. This means that in TypeScript, a Proxy(new API(), {...}) retains its identity as an API.

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

What sets apart `lib.es6.d.ts` from `lib.es2015.d.ts` in TypeScript?

I'm a bit confused about ES6 and ES2015. In TypeScript, there are two type declarations available for them: lib.es6.d.ts and lib.es2015.d.ts. Can someone explain the difference between these two? And which one is recommended to use? ...

Moving the legend around in vue-chartJS

As someone just starting out with Vue-ChartJs, I've become quite intrigued by this: https://i.sstatic.net/j1S0z.png I'm wondering how to move the legend to the bottom of the graph. Can anyone help me with that? ...

Creating a dynamic nested form in AngularJS by binding data to a model

Creating a nested form from a JSON object called formObject, I bind the values within the object itself. By recursively parsing the values, I extract the actual data, referred to as dataObject, upon submission. To view the dataObject in a linear format, r ...

How can you access the bound value in Vue JS?

I am struggling to access and display the value of model.title in the console when a click event is triggered. In my json file, there are a total of 3 records. The first model's title is IRIS. When I click on the link, I want it to be displayed in the ...

Struggling with uploading content to the Express backend using Axios in conjunction with React

I am currently utilizing React and Nextjs in conjunction with an Express back-end running on port 3001. My goal is to have the sign-up page successfully post the information to the database, however, I am encountering a series of errors. UPDATE - RESOLVE ...

React useState is not updating the onClick event as expected

I'm facing a challenge with the code I'm currently working on. The issue arises when trying to handle controlled inputs and passing API responses to a child component, resulting in only rendering the last value. Below is the portion of the code ...

Having trouble sending the information to Parse.com using the website

I am a beginner with the Parse database and I am currently working on implementing a code that allows users to sign up, with their information stored in the Parse database. However, I am encountering an issue where the values are not uploading as expected. ...

Struggling with "Content" not being recognized in Typescript PouchDB transpilation errors?

I have been diligently working on an Ionic app for the past three months with no major issues during development or deployment to mobile devices. However, yesterday I encountered a frustrating NPM dependency problem while trying to deploy to mobile. In an ...

The type 'void' cannot be assigned to type ... (User-defined type)

After spending several days researching this particular error, I am still struggling to fully comprehend it in order to resolve the issue within my code. The challenge I am facing involves retrieving data from the cloud using Firebase and loading it into ...

Having trouble viewing the page of a new package you published on the NPM Website?

Today, I officially released an NPM package called jhp-serve. It can be easily installed using npm install or run with npx. You can even find it in the search results here: https://www.npmjs.com/search?q=jhp. However, when attempting to view its page by cl ...

Is it possible to assign the active tab using setState from an array of tabs in the current window?

My Chrome Extension contains a feature that displays Link Previews of the current active tab, but there is a noticeable delay as it calls an API to render the preview. To improve user experience, I want to optimize this process by prerendering all tab URLs ...

`Steps to overcome cross-domain issues when using AJAX`

I am trying to tackle the issue of AJAX cross-domain errors. The specific error message I encounter in Chrome is as follows: XMLHttpRequest cannot load http://'myaddress'/TEST/user/login/testuserid . Request header field Access-Control-Allow-O ...

Minimizing assets in Angular 7 by running the command ng build --prod

After running ng build --prod, the JavaScript and CSS files located in /assets are not being minified. Is there a way to minify these files? I've checked the angular documentation but couldn't find any relevant information. ...

Reveal the MongoDB database connection to different sections within a Next.js 6 application

Currently developing an application using Next.js v6, and aiming to populate pages with information from a local mongodb database. My goal is to achieve something similar to the example provided in the tutorial, but with a twist - instead of utilizing an ...

Ways to convert a for loop with asynchronous operation embedded within

In a unique situation on my webpage, I have a varying number of grids. When the user clicks a button, I need to perform the following steps: For each grid Retrieve data from the grid Prompt the user to input additional data in a dialog box Execute a $.p ...

ReactJS component not triggering OnChange event in IE 11

While exploring the React.js documentation, I came across a suggestion to use the onChange event for text areas. Interestingly, when I tried pasting some text into an empty IE 11 text area, the onChange event failed to trigger. Surprisingly, it worked perf ...

avoidable constructor in a react component

When it comes to specifying initial state in a class, I've noticed different approaches being used by people. class App extends React.Component { constructor() { super(); this.state = { user: [] } } render() { return <p>Hi</p> ...

utilizing the default placeholder image for videos and audios within the tags

In my web application, users can share music and videos that will be stored on a server used by the application. Some audio/video files come with posters or thumbnails attached to them. I want to display these posters along with the audio or video using HT ...

Enhance your data retrieval from XPATH using Javascript

Here is the layout of an HTML template I am working with: <div class="item"> <span class="light">Date</span> <a class="link" href="">2018</a> (4pop) </div> <div class="item"> <span class="light">From</sp ...

Passing parent HTML attributes to child components in Angular 2

Is there a way to pass HTML attributes directly from parent to child without creating variables in the parent's .ts class first? In the sample code below, I am trying to pass the "type=number" attribute from the parent to the app-field-label component ...