Creating a type-safe method wrapper in TypeScript based on function names

Many Q&As discuss creating a function wrapper in TypeScript, but my question is how to do the same with named methods. I am interested in writing something similar to this JavaScript code:

  function wrap(API, fnName, fn) {
     const origFn = API.prototype[fnName];
     API.prototype[fnName] = function(...args) {
        fn(...args);
        return origFn.call(this, ...args);
     };
   }

   wrap(CanvasRenderingContext2D, 'fillRect', function(x, y, w, h) {
      console.log(x, y, w, h);
   });
   
   // --- check it worked ---
   const ctx = document.querySelector('canvas').getContext('2d');
   ctx.fillRect(10, 20, 100, 40);
<canvas></canvas>

Additionally, I would like to be able to use lists of functions or functions on objects like this:

function wrap(API, fnName, fn) {
     const origFn = API.prototype[fnName];
     API.prototype[fnName] = function(...args) {
        fn(...args);
        return origFn.call(this, ...args);
     };
   }

   const wrappers = {
     fillRect(x, y, w, h) {
       console.log('fill:', x, y, w, h);
     },
     strokeRect(x, y, w, h) {
       console.log('stroke:', x, y, w, h);
     },
   };

   for (const [name, fn] of Object.entries(wrappers)) {
     wrap(CanvasRenderingContext2D, name, fn);
   }
   
   // --- check it worked ---
   const ctx = document.querySelector('canvas').getContext('2d');
   ctx.fillRect(10, 20, 100, 40);
   ctx.strokeRect(40, 70, 20, 40);
<canvas></canvas>

Is it possible to write either of these forms in a type-safe manner in TypeScript? Can the function associated with the method match types based on the names of the functions?

Note: In my actual scenario, the functions will perform more than just console.log and will need to ensure they are using the types correctly.

Answer №1

If our main concern is the experience of callers using the wrap() function and we are not too worried about the compiler checking its implementation, then the code can be structured as follows:

function wrap<
  K extends PropertyKey,
  T extends Record<K, (...args: any) => any>
>(
  API: { prototype: T },
  fnName: K, 
  fn: T[K]
) {
  const origFn = API.prototype[fnName];
  API.prototype[fnName] = function (this: T, ...args: any) {
    fn(...args);
    return origFn.call(this, ...args);
  } as any;
}

In this version of wrap(), it is designed to be generic with types K for fnName and T for the instance type of API. It enforces that fnName is a literal type like "fillRect", and that API has a function property at key K.

The use of generics allows TypeScript to infer the types K and

T</code based on the arguments provided when calling <code>wrap()
. This ensures that the correct types are used throughout the function.

Additionally, the fn parameter is of type T[K], which corresponds to the type of API.prototype[fnName]. This means that fn is expected to have the same function signature as the original function.


Let's test out this implementation with an example:

wrap(CanvasRenderingContext2D, 'fillRect',
  function (x, y, w, h) {
    console.log(x.toFixed(), y.toFixed(), w.toFixed(), h.toFixed());
  });

By providing "fillRect" for K and CanvasRenderingContext2D for T, TypeScript infers the function signatures correctly. The call signature is seen as:

/* function wrap<"fillRect", CanvasRenderingContext2D>(
     API: { prototype: CanvasRenderingContext2D; }, 
     fnName: "fillRect", 
     fn: (x: number, y: number, w: number, h: number) => void
   ): void */

This means that fn() expects four parameters of type number, which TypeScript automatically infers from context without requiring manual annotations.


Overall, this approach should work well in most cases. More complex scenarios, such as loops or pre-declared fn arguments, may require additional considerations within the TypeScript language but are unrelated to the functionality of wrap() itself.

Link to Playground for testing

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

During rendering, the instance attempts to reference the "handleSelect" property or method which is not defined

I've incorporated the Element-UI NavMenu component into my web application to create a navigation bar using Vue.JS with TypeScript. In order to achieve this, I have created a Vue component within a directory called "navBar," which contains the follow ...

Unexpected lint errors are being flagged by TS Lint in Visual Studio Code out of nowhere

After a 5-week break from VS Code and my computer due to vacation, I was surprised to see TS lint errors popping up out of nowhere. These errors were completely incorrect and appearing in files that had previously been error-free. It's as if the linte ...

Customizing MUI V5 Variants

I'm having trouble customizing the variant options in MUIPaper, and I can't figure out what mistake I'm making. The available types for the component are: variant?: OverridableStringUnion<'elevation' | 'outlined', Pape ...

"Silently update the value of an Rxjs Observable without triggering notifications to subscribers

I'm currently working on updating an observable without alerting the subscribers to the next value change. In my project, I am utilizing Angular Reactive Forms and subscribing to the form control's value changes Observable in the following manner ...

Injecting dynamic templates in Angular 7

Let me simplify my issue: I am currently using NgxDatatable to display a CRUD table. I have a base component named CrudComponent, which manages all CRUD operations. This component was designed to be extended for all basic entities. The challenge I am en ...

Can an excessive amount of classes cause my Angular application to run sluggishly?

Within my Angular 7 application, I have generated approximately 200 to 300 classes for model types (e.g. component.model.ts) solely for type checking purposes. I have not instantiated any objects from these classes. As I navigate through the application, ...

Encountering an issue with creating an App Runner on AWS CDK

Attempting to deploy my application using App Runner within AWS via CDK. Utilizing the reference from https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-apprunner.Service.html. Upon deployment, encountering the following error: create_failed: R ...

What is the best way to set up an endpoint in Angular for image uploading?

Using the Kolkov Angular editor in my Angular application, I have successfully created a rich text editor. Currently, I am looking to upload images from the editor to the server. I already have a function in place that takes a file as an argument and send ...

Tips for transfering variables from an electron application to the backend of an Angular project

My goal is to develop a website and desktop application using the same code base. However, due to some minor differences between the two platforms, I need a way for my Angular app to distinguish whether it has been called from the web or from Electron. I& ...

Rxjs: Making recursive HTTP requests with a condition-based approach

To obtain a list of records, I use the following command to retrieve a set number of records. For example, in the code snippet below, it fetches 100 records by passing the pageIndex value and increasing it with each request to get the next 100 records: thi ...

Is it possible for FormArray to return null?

Hello there. I've attempted various methods, but none of them seem to be effective. Currently, I am working on this task where I need to start a formArray for emails. email: [testestest] However, what I have is: email: [testestest] I'm encoun ...

ESLint not functioning properly on TypeScript (.ts and .tsx) files within Visual Studio Code

After installing the ESLint extension in VSC, I encountered an issue where it was no longer working on the fly for my React project when I introduced Typescript. In the root of my project, I have a .eslintrc file with the following configuration: { "pa ...

Contrasting `Function` with `(...args: any[]) => any`

Can you explain the difference between Function and (...args: any[]) => any? I recently discovered that Function cannot be assigned to (...args: any[]) => any. Why is that so puzzling? declare let foo: Function; declare let bar: (...args: an ...

A method for consolidating multiple enum declarations in a single TypeScript file and exporting them under a single statement to avoid direct exposure of individual enums

I am looking to consolidate multiple enums in a single file and export them under one export statement. Then, when I import this unified file in another file, I should be able to access any specific enum as needed. My current setup involves having 2 separ ...

How to target a particular Textfield in React with its unique identifier

I'm working on a component that contains various Textfields and need to access specific IDs. For example, I want to target the textfield with the label 'Elevator amount'. I attempted the following code snippet but am unsure of how to correct ...

Utilize localStorage.getItem() in conjunction with TypeScript to retrieve stored data

Within my codebase, I have the following line: const allGarments = teeMeasuresAverages || JSON.parse(localStorage.getItem("teeMeasuresAverages")) || teeMeasuresAveragesLocal; Unexpectedly, Typescript triggers an alert with this message: Argument ...

Issue with MUI 5 Button component not receiving all necessary props

Currently, I am attempting to create a customized MUI5-based button in a separate component with the following code: import {Button, buttonClasses, ButtonProps, styled} from '@mui/material'; interface MxFlatButtonProps extends Omit<ButtonProp ...

TypeScript seems to be failing to detect the necessary checks before they are used

I've been pondering on how to ensure TypeScript acknowledges that I am verifying the existence of my variables before using them. Below is the code snippet : Here's the function responsible for these checks: function verifyEnvVars(){ if (!proc ...

Error encountered during the deployment of Ionic 3 with TypeScript

After completing the development of my app, I ran it on ionic serve without any issues. However, when compiling the app, I encountered the following error message. Any assistance in resolving this matter would be greatly appreciated. [15:40:08] typescri ...

MasterNG - Submitting form details and uploading files with a button press

Our Angular project involves a form with various form fields along with PrimeNG FileUpload, and our goal is to send the form data along with the selected files in one go. Despite researching the documentation and numerous examples online, we couldn't ...