What is the best way to create an instance method in a subclass that can also call a different instance method?

In our programming project, we have a hierarchy of classes where some classes inherit from a base class.

Our goal is to create an instance method that is strongly-typed in such a way that it only accepts the name of another instance method as input.

We decided to implement this method in the base class so that all derived classes can inherit it. However, we are facing challenges in correctly typing this method.

After brainstorming, we came up with a solution using keyof this:

abstract class Animal {
    abstract species: string;

    breathe() {
        console.log(`Ahh`);
    }

    exec(methodName: keyof this) {
        (this[methodName] as () => void)();
    }
}

class Cat extends Animal {
    lovesCatnip = true;
    species = `felis catus`;

    meow() {
        console.log(`Meow`);
    }
}

class Dog extends Animal {
    chasesCars = true;
    species = `canis familiaris`;

    bark() {
        console.log(`Woof`);
    }
}

const fido = new Dog();
fido.exec(`breathe`); // Good
fido.exec(`bark`); // Good
fido.exec(`species`); // Bad
fido.exec(`chasesCars`); // Bad
const snowball = new Cat();
snowball.exec(`breathe`); // Good
snowball.exec(`meow`); // Good
snowball.exec(`species`); // Bad
snowball.exec(`lovesCatnip`); // Bad

You'll notice our comments indicate that although exec uses keyof this, it currently allows any instance property instead of just instance methods.

Previously, we raised a similar issue and found a workaround when exec was in the same class as the methods. However, the solution doesn't work when exec is part of a superclass.

Can you help us figure out how to type exec so that it only accepts names of instance methods when called on a subclass?

Answer №1

One interesting fact about TypeScript is that it lacks a built-in utility type called KeysMatching<T, V>. This utility type would essentially extract the subset of keys from keyof T where the properties at those keys are guaranteed to match type V (meaning

T[KeysMatching<T, V>] extends V
). There has been a feature request open for this on GitHub at microsoft/TypeScript#48992. Even though this utility type is missing, users can implement their own versions of KeysMatching<T, V> to handle specific use cases.

type KeysMatching<T, V> =
  { [K in keyof T]: T[K] extends V ? K : never }[keyof T]

This custom utility type leverages mapped types and indexed access types to generate a union of property value types from the properties of T. The resulting union represents all keys K where T[K] extends V.

To test out this custom utility type, let's consider the following example:

class Test {
  foo: string = "";
  bar() { };
  baz = (x?: string) => x?.toUpperCase()
}
type TestKeys = KeysMatching<Test, () => void>;
// type TestKeys = "bar" | "baz"

In this example, we retrieve the keys of the Test class that can be assigned to ()=>void. As expected, this includes "bar" and "baz", but not "foo".


Using this custom utility type becomes slightly trickier when incorporating it into classes with methods like exec():

abstract class Animal {
  abstract species: string;

  breathe() {
    console.log(`Ahh`);
  }

  exec(methodName: KeysMatching<Omit<this, "exec">, () => void>) {
    this[methodName](); // error
  }
}

The above implementation starts encountering problems due to circular definitions. To address these issues, one workaround involves using an Omit utility type to exclude problematic keys such as "exec". Additionally, performing operations like this[methodName]() might trigger errors because the compiler struggles to understand the generic behavior necessary to verify certain constraints.

To resolve these errors, developers can resort to employing type assertions within the code:

abstract class Animal {
  abstract species: string;

  breathe() {
    console.log(`Ahh`);
  }

  exec(methodName: KeysMatching<Omit<this, "exec">, () => void>) {
    (this[methodName] as () => void)();
  }
}

While there may still be further tweaks possible to enhance behaviors inside the exec() method, utilizing type assertions offers a simple fix for immediate concerns.


When testing the functionality of exec() within different instances like Dog and Cat, robust input validation is observed along with helpful IDE autosuggestions for developers.


However, challenges arise when dealing with generic or polymorphic types that hinder the compiler's understanding of KeysMatching<T, V>. Until potential future enhancements land, developers might need to navigate around these limitations to ensure smoother experiences across various scenarios.

Link to TypeScript Playground

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

After clicking on the "Delete Rows" button in the table, a white color suddenly fills the background in Angular Material

When the dialog box pops up, you'll see a white background color: https://i.stack.imgur.com/EflOx.png The TypeScript code for this action can be found in config-referrals.component.ts openDialog(action, obj) { this.globalService.configA ...

The SetInterval function will continue to run within a web component even after the corresponding element has been removed from the

I am currently engaged in developing a straightforward application that coordinates multiple web components. Among these components, there is one that contains a setInterval function. Interestingly, the function continues to run even after the component it ...

Event typeORM on afterUpdate in NestJS

After every update of my data in the log table, I want to insert an entry into another table. To achieve this, I have created an EntitySubscriberInterface. The event is triggering correctly, but the entity array does not include the updated id. async afte ...

What is the best method for storing a third-party image in cache?

Running my website, I aim to achieve top-notch performance scores using LightHouse. I have successfully cached all the images I created (Cache-Control: public, max-age=31536000). Unfortunately, third-party website images are not cached. How can I cache t ...

Having difficulty customizing Mui Accordion with Styled Utility implementation

I am having trouble overriding the CSS for an Accordion using Mui styled utility. I am trying to apply a custom CSS class, but there seems to be an underlying class that is causing issues in my code. Here is the Mui class snippet: <div class="MuiPa ...

How can I incorporate a feature in my Angular application that allows users to switch between different view types, such as days, using JavaScript

Greetings, community! I am currently utilizing version 5 of the fullcalendar library from https://fullcalendar.io/ in my Angular 9 application. I have noticed that the calendar offers various options to change the view type as shown below: https://i.stac ...

Exploring the method to deactivate and verify a checkbox by searching within an array of values in my TypeScript file

I am working on a project where I have a select field with checkboxes. My goal is to disable and check the checkboxes based on values from a string array. I am using Angular in my .ts file. this.claimNames = any[]; <div class="row container"> ...

Tips for displaying or concealing a div using Angular Js

I have set up two div elements with a dropdown control in one named div1. I am looking to hide div2 until a value is selected in the dropdown. How can I show or hide a div based on ng-change, ensuring that div2 remains hidden until a value is selected? Cod ...

Is there a way to position the Image component from next/image using absolute positioning?

Is it possible to use an element Image from 'next/image' with the CSS style { position: absolute; left: 50% }? It appears that it is not being applied. For example: import React from 'react' import Image from 'next/image' imp ...

What is the proper way to call document methods, like getElementByID, in a tsx file?

I am currently in the process of converting plain JavaScript files to TypeScript within a React application. However, I am facing an error with document when using methods like document.getElementById("login-username"). Can you guide me on how to referen ...

leveraging third party plugins to implement callbacks in TypeScript

When working with ajax calls in typical javascript, I have been using a specific pattern: myFunction() { var self = this; $.ajax({ // other options like url and stuff success: function () { self.someParsingFunction } } } In addition t ...

Export interface for material-ui wrapper to cast any type in TypeScript (React)

I work with React using TypeScript. Recently, I encountered an issue with exporting. I'm creating an interface that encapsulates components from Material-ui. Here is a simplified example: Wrapping.tsx import { default as Component, ComponentProps ...

Stop unnecessary updating of state in a global context within a Functional Component to avoid re-rendering

I am currently working with a Context that is being provided to my entire application. Within this context, there is a state of arrays that store keys for filtering the data displayed on the app. I have implemented this dropdown selector, which is a tree s ...

Guide on accessing a modal component in Angular?

I have an Edit Button on my component called SearchComponent. When the user clicks this button, it currently redirects them to another component named EditFormComponent using navigateByUrl('url-link'). However, I would like to enhance the user ex ...

Error occurs in Angular Mat Table when attempting to display the same column twice, resulting in the message "Duplicate column definition name provided" being

What is the most efficient method to display a duplicated column with the same data side by side without altering the JSON or using separate matColumnDef keys? Data: const ELEMENT_DATA: PeriodicElement[] = [ {position: 1, name: 'Hydrogen', wei ...

What is the best way to output data to the console from an observable subscription?

I was working with a simple function that is part of a service and returns an observable containing an object: private someData; getDataStream(): Observable<any> { return Observable.of(this.someData); } I decided to subscribe to this funct ...

Could you provide an explanation of the styled() function in TypeScript?

const Flex = styled(Stack, { shouldForwardProp: (prop) => calcShouldForwardProp(prop), })<LayoutProps>(({ center, autoWidth, autoFlex, theme }) => ({ })); This syntax is a bit confusing to me. I understand the functionality of the code, b ...

Tips for securely encrypting passwords before adding them to a database:

While working with Nest.Js and TypeORM, I encountered an issue where I wanted to hash my password before saving it to the database. I initially attempted to use the @BeforeInsert() event decorator but ran into a roadblock. After some investigation, I disc ...

Tips for adjusting the size of icons in Ionic Framework v4 and Angular 7

The library ngx-skycons offers a variety of icons for use in projects. If you're interested, check out the demo here. I'm currently incorporating this icon library into an Ionic project that utilizes Angular. While the icons work perfectly, I&ap ...

Using ngIf to validate an empty string in Angular 5

I need assistance with validating an empty string retrieved from a server Although it is usually straightforward, it's just not working as expected <div class="ui-g-2 info-txt" *ngIf="appointment.Notes !==null || appointment.Notes !== ...