What is the proper way to invoke an instance method through an inherited method using its name and ensuring the arguments are correctly typed?

We have created a foundational class that will be extended numerous times. Our goal is to implement an `exec` method on this base class, which will take the name and arguments of a method from a derived class and execute it.

However, we are encountering an issue with the typing of the arguments in the method of the derived class. The code snippet below demonstrates the error:


        class BaseClass {
            exec<
                Self extends Record<MethodKey, (...args: any) => void>,
                MethodKey extends keyof Self,
                Method extends Self[MethodKey],
            >(
                this: Self,
                methodKey: MethodKey,
                ...args: Parameters<Method>
            ) {
                this[methodKey](...args);
            }
        }

        class MyClass extends BaseClass {   
            constructor() {
                super();
                this.exec(`sayMessage`, `Hello`); // Error: Argument of type '["Hello"]' is not assignable to parameter of type 'Parameters<this["sayMessage"]>'.
            }

            sayMessage(message: string) {
                console.log(message);
            }
        }
    

TS Playground link

Why does this setup fail, and how can we correctly define the typing for the `exec` method to accept arguments from another method?

Answer №1

The main issue at hand is the inference of the Self type as the polymorphic this type, which is a generic type constrained to the current class instance type. This leads to issues with the Parameters utility type, implemented as a conditional type, causing args to be inferred as

Parameters<this["sayMessage"]>
, a generic conditional type that the compiler struggles to evaluate without precise information about this.

To simplify this for the compiler, it's better to provide exact type values for inference. Instead of having args as a complex entity, make it a generic type parameter A, and adjust other components accordingly. For instance:

class BaseClass {
    exec<
        A extends any[],
        K extends PropertyKey
    >(
        this: Record<K, (...args: NoInfer<A>) => void>,
        methodKey: K,
        ...args: A
    ) {
        this[methodKey](...args);
    }
}

This modified version retains two generic parameters - A, limited to a list type, and K, akin to your MethodKey, restricted to PropertyKey, encompassing "keylike" types such as string | number | symbol.

In defining the this parameter, specify it as

Record<K, (...args: NoInfer<A>) => void>
. Essentially, this denotes "something containing a property with key K that is a function accepting argument list of type A", while using the new NoInfer utility type from TypeScript 5.4 to prevent unnecessary inference attempts.

Upon application, the compiler now operates smoothly. It can easily recognize when this aligns with a type like

{sayMessage: (...args: string)=>void}
, eliminating the need to handle generic conditional types entirely:

class MyClass extends BaseClass {
    constructor() {
        super();
        this.exec(`sayMessage`, "hello"); // okay
    }

    sayMessage(message: string) {
        console.log(message);
    }
}

Click here to access the code on the playground

Answer №2

It's interesting how defining Args before the method seems to make it work, even though I'm not entirely sure why:

class BaseClass {
    exec<
        Args extends Array<unknown>,
        Self extends Record<MethodKey, (...args: Args) => void>,
        MethodKey extends keyof Self,
    >(
        this: Self,
        methodKey: MethodKey,
        ...args: Args
    ) {
        this[methodKey](...args);
    }
}

class MyClass extends BaseClass {
    constructor() {
        super();
        this.exec(`sayMessage`, 32);
    }

    sayMessage(message: number) {
        console.log(message);
    }
}

TS Playground link

Credit to this chat in the Typescript discord:

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

Using jQuery to extract the HTML content of an element while removing the style attribute

Is there a way to retrieve the raw HTML of an element, or obtain the HTML without the "style" attribute applied? Let's consider the example below: <div class="outer"> <div class="inner"> some text </div> </div> A ...

Issue encountered: Document not being saved by Mongoose, error message received: "MongoClient connection must be established"

Currently, I am attempting to establish a connection with MongoDb using mongoose. Below is the code snippet that I have implemented for this purpose: controller.js const conn = mongoose.createConnection(db, { useNewUrlParser: true, ...

What changes can I make to my method in order to utilize async/await?

Within my React application, I have implemented a post request to the server using axios: onSubmit = async (results) => { try { const response = await axios.post("http://localhost:8080/simulate/", results); this.setState({results: ...

What are the steps to set up ChartJS on a personal computer?

Currently, I am working on creating charts with ChartJS using the CDN version. However, I would like to have it installed directly on my website. After downloading ChartJS v4.1.1, I realized that it only contains typescript files. Since I cannot use TS fil ...

Is the RadioButton change jQuery function malfunctioning following modifications to the DOM?

After updating the HTML of the radio button (with the same name) through AJAX based on certain logic (such as rate or duration changes), I noticed that the change(function(e) method worked perfectly before the DOM was updated. However, once the DOM chang ...

Encountering the error "currentTime" property is not defined while executing JavaScript in Selenium with Python

Is there a way to navigate to a specific time while watching a video online? Although I have successfully achieved a similar goal on YouTube (check out the post here), I have encountered issues with the same method on a different website. Unfortunately, I ...

Deactivate the form outside of normal business hours

I need to create a form for a delivery service that only operates from 9am to 9pm. If a user submits the form outside of these hours, I want to redirect them to a page displaying the company's operating hours instead of a thank you page. For instance ...

Displaying a random element from the state array in React Native

I'm attempting to display a random item from the state array, with the possibility of it changing each time the page reloads. Here's what I have tried so far, any suggestions or ideas are welcome! This is my current state: state = { randomIt ...

Is it possible to change the transition behavior within a Vue component?

Is there a way to modify or replace transitions within a Vue component? I am currently using Buefy components for my website, but I have encountered an issue with certain components like collapse that have a slot with a fade transition that I do not pref ...

Here's a way to programmatically append the same icon to multiple li elements generated using document.createElement:

I am new to creating a to-do app and need some guidance. When a user enters a task, I successfully create a list item for that task. However, I am unsure how to add a delete icon next to the newly created li element. Can someone please help me out? Below ...

Showcase -solely one property object- from numerous property objects enclosed within an array

Hello, I am seeking assistance as I have recently begun learning about angularJS. I am working with objects that have keys such as: scope.eventList=[]; for(){ var event = { id : hash, ...

Innovative guidelines originating from a resource attribute

I am facing a challenge with a repeater for a resource that has an attribute containing angular directives mixed with text. My goal is to display form inputs dynamically based on the object's property. <ul> <li ng-repeat="action in actions ...

Getting command line argument parameters in React can be achieved by utilizing the `process.argv`

Is there a way to retrieve command line argument parameters in React? I currently have a React codebase that is utilizing Webpack. When running the following commands - npm run build -- --configuration=dev or npm run build -- --configuration=qa I need t ...

JS: delay onClick function execution until a page refresh occurs

Currently, I am working on a WordPress site that involves a form submission process. Upon successful submission, a new post is created. After the user submits the form, I have implemented JavaScript to prompt them to share a tweet with dynamically prepopu ...

Using select2 scss from the node_modules folder

Looking for help with importing select2 scss from node_modules. I successfully installed the select2 by running the command: npm install select2 which created a select2 folder in the node_modules directory. Can anyone guide me on how to import the css f ...

When the page loads, should the information be transmitted in JSON format or should PHP be responsible for formatting it?

I'm considering whether it would be more server-efficient and effective to send data to the user in JSON format upon page load, with JavaScript handling the conversion into readable information. For instance, when a user visits my index page, instead ...

Tips for simulating an axios request that returns an image buffer

I am attempting to simulate an axios api call that returns an image buffer as shown below: <Buffer ff d8 ff e0 00 10 4a 46 49 46 00 01 01 00 00 01 00 01 00 00 ff e1 00 de 45 78 69 66 00 00 49 49 2a 00 08 00 00 00 06 00 12 01 03 00 01 00 00 00 01 00 ... ...

Rotating an object in Three.js along the radius of a sphere

I want to position an object at the end of a sphere's radius based on its coordinates (x, y, z). In traditional mathematical formulas for the coordinates of a point on a sphere, we use: var x = radius * Math.cos(phi) * Math.cos(theta); var y = radiu ...

Performing AJAX requests to dynamically update multiple DIVs

Encountering difficulties with adding additional input fields to a page, each of which contains jquery code for appending more select boxes related to them. The base html setup is as follows: <p id="add_field"> … </p> <div class="show_sub ...

Issue with scroll being caused by the formatting of the jQuery Knob plugin

I am currently utilizing the jQuery Knob plugin and I am looking to have the displayed value shown in pound sterling format. My goal is for it to appear as £123456. However, when I attempt to add the £ sign using the 'format' hook, it causes is ...