Using TypeScript to wrap a class with a Proxy object

I've been working on a function that takes an API interface (I've provided a sample here) and creates a Proxy around it. This allows me to intercept calls to the API's methods, enabling logging, custom error handling, etc. I'm running into some issues with types that are causing headaches. While similar in concept to another question I posted (Writing wrapper to third party class methods in TS), this approach is different based on feedback I received.

Currently, I'm facing an error message:

Element implicitly has an 'any' type because expression of type 'string | symbol' can't be used to index type 'API'. No index signature with a parameter of type 'string' was found on type 'API'
. It makes sense since sayHello isn't strictly considered a string in TypeScript, but I'm unsure of the best way to access methods on this class without using property accessor notation.

class API {
 sayHello(name: string) {
     console.log(“hello “ + name)
     return name

  }
}

export default <T extends API>(
  api: T,
) =>
  new Proxy(api, {
    get(target, prop) {
      if (typeof target[prop] !== "function") { 
        return target[prop]; 
      }
      return async (...args: Parameters<typeof target[prop]>) => {

        try {
          const res = await target[prop](...args); 
         
          return res
        } catch (e) {
          
        }
      };
    },
  });

Can this be achieved in TypeScript?

Answer №1

In TypeScript currently, there is no support for handling cases where a Proxy object has a different type from its target object. This issue has been raised as a feature request on the official GitHub repository and it's unsure when or if this situation will be addressed.

As a workaround, if you need to achieve this behavior, you'll have to manually specify the expected return type of your proxy function and make use of type assertions within the implementation to suppress any potential errors. The responsibility of ensuring type safety will lie with you, as the compiler won't offer much assistance in this scenario.

One possible solution can be written as follows:

const prox = <T extends API>(api: T) => new Proxy(api, {
  get(target: any, prop: string) {
    if (typeof target[prop] !== "function") {
      return target[prop];
    }
    return async (...args: any) => {
      try {
        const res = await target[prop](...args);
        return res;
      } catch (e) { }
    };
  },
}) as any as { [K in keyof T]: AsyncifyFunction<T[K]> };

type AsyncifyFunction<T> = T extends (...args: infer A) => infer R ?
  (...args: A) => Promise<Awaited<R>> : T;

The concept here is that calling `prox(api)` will produce a modified version of the original `api` object where non-function properties remain unchanged but all function properties are transformed into asynchronous versions that return Promises. If the type of `api` is `T`, then `prox(api)` becomes a mapped type `{ [K in keyof T]: AsyncifyFunction<T[K]> }`, with `AsyncifyFunction<T>` being a conditional utility type that handles the transformation of each property type.


We can now test this implementation:

class Foo extends API {
  str = "abc";
  double(x: number) { return x * 2 };
  promise() { return Promise.resolve(10) };
}
const y = prox(new Foo());
/* const y: {
    str: string;
    double: (x: number) => Promise<number>;
    promise: () => Promise<number>;
    sayHello: (name: string) => Promise<void>;
} */

According to the compiler, the variable `y` includes a property `str` of type `string`, along with other properties which are asynchronous methods returning promises. Notably, the method `promise()` returns `Promise<number>` instead of `Promise<Promise<number>>`.

To verify functionality, let's proceed with some demonstration:

console.log(y.str.toUpperCase()) // "ABC"
y.sayHello("abc") // "helloabc"
y.double(123).then(s => console.log(s.toFixed(2))) // "246.00"
y.promise().then(s => console.log(s.toFixed(2))) // "10.00"

All seems in order!

Link to Playground for testing

Answer №2

If you want to enhance your class, consider incorporating an index signature like this:

class Data {

 /* -- index signature -- */
 [index:string|symbol]: any;

 showInfo(info: string) {
    console.log("displaying info" + info)
  }
}

export default <T extends Data>(
  data: T,
) =>
  new Proxy(data, {
    get(target, property) {
      if (typeof target[property] !== "function") {
        return target[property];
      }

      /* -- need to save it in a variable -- */
      const func = target[property];

      return async (...args: Parameters<typeof func>) => {

        try {
          const result = await target[property](...args);
          // perform operations
          return result.data;
        } catch (error) {
          // handle exceptions
        }
      };
    },
  });

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

"Introducing the new Next.Js 14 sidebar featuring a sleek hamburger menu

I am in the process of developing a chat application. Currently, I have a sidebar that displays existing conversations and I would like to make it responsive by implementing open and close functionalities for mobile devices. Here is the code snippet for m ...

What steps should I take to create a React component in Typescript that mimics the functionality of a traditional "if" statement?

I created a basic <If /> function component with React: import React, { ReactElement } from "react"; interface Props { condition: boolean; comment?: any; } export function If(props: React.PropsWithChildren<Props>): ReactElement | nul ...

Having trouble with basic authorization for Facebook API using JavaScript?

I am having an issue with my source code. When I run it, I receive an alert that says "No Response" and then the Facebook page displays an error message stating "An error occurred with MYAPP. Please try again later" <div id="fb-root"></div> & ...

Add both single (' ') and double (" ") quotes within the `document.querySelector` code

Given an array of attributes like the following: let elementsArray = [ '[name="country_code"]', '[name="user_tel_code"]', '[name="countryCode"]' ]; If I aim to iterate through them using a ...

Creating a dynamic slideshow with automated arrow navigation is simpler than you may think

I have successfully tested the slideshow and it is functioning perfectly without any issues. I would like to have a dual slideshow setup (slideshow 1 and slideshow 2) with next and previous buttons, but I am interested in adding an automatic sliding featur ...

What steps can be taken to ensure that any new elements generated by JavaScript do not disappear upon refreshing the page

I am currently working on a project where I am creating a list by collecting user information and storing it in a MySQL database. When the user clicks the 'add' button, a new element is added to the bottom of the existing list which is coded in H ...

Duplicating labels with JavaScript

I need assistance with copying HTML to my clipboard. The issue I am encountering is that when I try to copy the button inside the tagHolder, it ends up copying <span style="font-family: Arial; font-size: 13.3333px; text-align: center; background-color: ...

Submitting forms that contain files using jQuery and AJAX

Despite searching through numerous questions on this topic, I have yet to find a solution to my specific issue. My objective is to successfully submit an entire form, potentially containing files as well. The code I currently have is not working: $(target ...

What is the best way to add bold formatting to text enclosed in parentheses using javascript/jquery?

How can I use jQuery or JavaScript to make the text inside parentheses bold? For example: "what is your Age (in Year)." I need assistance in making the text within parentheses bold using either jQuery or JavaScript. Can someone help me with this task? ...

A guide on implementing Angular-DataTable within a TypeScript project

Hey everyone, I'm currently working on a TypeScript and Angular application. To create a data table, I decided to use Angular-DataTable. After creating a sample application using it, I added the following code to my Controller: constructor(protecte ...

Add the component view to the webpage's body section

Using Angular 7 and ngx-bootstrap 4.0.1 Dependencies: "bootstrap": "3.3.7", "bootstrap-colorpicker": "2.5.1", "bootstrap-duallistbox": "3.0.6", "bootstrap-markdown": "2.10.0", "bootstrap-progressbar": "0.9.0", "bootstrap-slider": "9.8.0", "bootstrap-tags ...

Stop the stream coming from getUserMedia

After successfully channeling the stream from getUserMedia to a <video> element on the HTML page, I am able to view the video in that element. However, I have encountered an issue where if I pause the video using the controls on the video element a ...

Enhancing button functionality using jQuery and JavaScript for toggling behavior

I am facing a challenge with this code. The goal is to have a single script that can handle multiple audio elements on the page and toggle between PLAY and PAUSE buttons by adding or removing classes. Can anyone suggest any changes or additions that coul ...

Update class name in React component based on state change

My current setup involves the setting of active and class flags as shown below: constructor(props) { super(props); this.state = {'active': false, 'class': 'album'}; } handleClick(id) { if(this.state.active){ this.s ...

How can Vue handle passing an array in this scenario?

In my code snippet, I am attempting to build a simple form builder application. The goal is to include multiple select fields in the form. I encountered a problem with passing an array into a loop. Despite my efforts, the code did not work as expected. Ho ...

Utilize Ajax datatable to showcase information in a visually interactive format

I've been grappling with this problem for an entire day. Essentially, I have a table and I need to pass data in a multidimensional array $list through a datatable using AJAX. This way, I can JSON encode it and send it back for display: $('#table ...

Attempting the transformation of jQuery ajax requests to Angular's http service

I am looking to transition my existing mobile application from using jquery ajax to angularjs for handling user authentication with server-side validation. Here is the original jquery ajax code: function validateStaffUser(username, password) { var re ...

Executing the routing component prior to any other tasks

Having an issue where the ProductsService is fetching data from the server and storing it in an Array. The ProductsComponent serves as the parent component, while the ProductsListComponent and ProductListItemsComponent are its children components. The flow ...

Deciding whether an altered image has been successfully loaded

Currently, I am stuck on a minor point while creating a small gallery using jQuery. The code snippet looks like this: <script type="text/javascript> $(document).ready(function(){ $('#thumb1').click(function(){ $('#fullimage ...

Is there a way to set the submitted variable to true when the form group is submitted, then revert it to false when the user makes changes to the form?

With just one FormGroup, I ensure that when a user submits the form with errors the 'submitted' variable is set to true, displaying the errors. However, my challenge now is how to reset this variable to false when the user makes any changes after ...