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

Transferring data between components in Ionic 2: Service to Page

My service code includes two functions - one for creating a native storage with IP and port, and the other for retrieving values from the native storage. DatabaseService export class DatabaseService { ... public ip: string; public port: string; . ...

Error: The token 'export' in d3zoom is causing a syntax issue

I'm encountering issues with executing tests in Angular: ({"Object.<anonymous>":function(module,exports,require,__dirname,__filename,global,jest){export {default as zoom} from "./zoom.js"; ...

Is it advisable to encapsulate my entire Express server within a TypeScript class?

After working extensively with nodeJs, I have decided to explore developing applications in Typescript. Recently, I came across various blogs (like this one) that recommend wrapping modules and the app's entry point in a class when creating a RESTful ...

Utilize the object's ID to filter and display data based on specified criteria

I retrieved an array of objects from a database and am seeking to narrow down the results based on specific criteria. For instance, I want to display results only if a user's id matches the page's correct id. TS - async getResultsForId() { ...

What's the deal with the `return of ()` syntax?

Just came across this piece of code: https://i.sstatic.net/JZXP5.png Code snippet in typescript. The first line looks like: ... return of (true); Can someone explain this syntax to me? ...

What is the solution for breaking a querySnapshot in Firestore?

Is there a way to exit a querysnapshot loop prematurely? I attempted using a for loop, but I keep encountering the following error message. How can this error be resolved or is there an alternative method to break out of a snapshot loop? code return ...

Tips for positioning a div alongside its parent div

I have a unique setup with two nested divs that are both draggable. When I move the parent div (moveablecontainer), the child div (box.opened) follows smoothly as programmed. Everything works well in this scenario. However, when I resize the browser windo ...

Is there a tool in Node.js to set up a new project, similar to the scaffolding feature in Visual Studio for C# projects

Is there a way to efficiently create a node.js project with TypeScript and Express, and embed an SPA client using React and Redux templates written in TypeScript as well? Is there a scaffolding tool available to streamline this process, similar to the ea ...

Error in JSON format detected by Cloudinary in the live environment

For my upcoming project in Next.js, I have integrated a Cloudinary function to handle file uploads. Here is the code snippet: import { v2 as cloudinary, UploadApiResponse } from 'cloudinary' import dotenv from 'dotenv' dotenv.config() ...

Updating Angular 9 values using a fixed object

I am dealing with a patch value here where I simply pass an object to it. this.formPesquisar.controls['daniloTeste'].patchValue(this.dadosVisualizar.daniloTeste); However, I would like to pass a static object instead, something like: this.formPe ...

I'm curious about the type I can set for the first parameter of setState in TypeScript. Is there a way to pass a dynamically generated state object to setState?

When trying to pass a newState object to setState and add some additional properties under certain conditions, I encountered a type error: I attempted to define the new State as Pick<ItemListState, keyof ItemListState> but received a type error ...

How can I solve export issues from index.ts after publishing to NPM?

I have a package called this package which contains an index.ts file. The corresponding index.d.ts file that is located in the directory node_modules/@fireflysemantics/slice has the following content: export { EStore } from './EStore'; export { ...

Exploring the power of Next.js, Styled-components, and leveraging Yandex Metrica Session Replay

I'm currently involved in a project that utilizes Next.js and styled-components. In my [slug].tsx file: export default function ProductDetails({ product }: IProductDetailsProps) { const router = useRouter(); if (router.isFallback) { return ( ...

Compiling TypeScript files with an incorrect path when importing, appending "index" at the end of the @angular/material library

I'm currently working on creating a library to collect and distribute a series of Angular components across various projects, with a dependency on angular/material2. My objective is to eventually publish it on npm. However, I've encountered an i ...

Mapping Form Fields (with Formik)

Currently, the Formik/Yup validation setup in my form is working perfectly: export default function AddUserPage() { const [firstName, setFirstName] = useState(""); const [email, setEmail] = useState(""); return ( <div> <Formik ...

How can React TypeScript bind an array to routes effectively?

In my project, I am using the standard VisualStudio 2017 ASP.NET Core 2.0 React Template. There is a class Home included in the template: import { RouteComponentProps } from 'react-router'; export class Home extends React.Component<Rout ...

The exclude option in Nest JS middleware does not prevent the middleware from running on excluded routes

I'm having an issue with excluding certain routes from the middleware. The .exclude option doesn't seem to be working as expected, as the middleware is still being applied to the excluded routes. Here is the code for the Middleware: https://i.st ...

Mono repo project utilizing Angular 4+ and Typescript, enhanced with Bootstrap styling

Looking for a project to practice with Angular 4+ using Typescript and a Bootstrap template. Hoping for a setup where I can just run npm install and ng serve to start. Any recommendations for mono repos would be highly valued! ...

Unable to find the module... designated for one of my packages

Within my codebase, I am utilizing a specific NPM package called my-dependency-package, which contains the module lib/utils/list-utils. Moreover, I have another package named my-package that relies on my-dependency-package. When attempting to build the pr ...

Increase the size of the NativeScript switch component

Here is the code I am working with: .HTML <Switch style="margin-top: 10" (checkedChange)="onFirstChecked1($event)" row="0" col="1" horizontalAlignment="center" class="m-15 firstSwitchStyle"></Switch> .CSS .firstSwitchStyle{ width: 30%; ...