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

Leverage the exported data from Highcharts Editor to create a fresh React chart

I am currently working on implementing the following workflow Create a chart using the Highcharts Editor tool Export the JSON object from the Editor that represents the chart Utilize the exported JSON to render a new chart After creating a chart through ...

Issue: (SystemJS) XHR error (404) encountered in Angular2 Plnkrsandbox

The issue: https://i.sstatic.net/jUKBU.png https://plnkr.co/edit/910M73kwYKc8xPlSIU57?p=preview index <!DOCTYPE html> <html> <head> <base href="/"> <title>Angular 2.1.2 + TypeScript Starter Kit</title> <met ...

Mapping nested JSON to model in Angular2 - handling multiple API requests

Having trouble integrating two HTTP API Responses into my Model in Angular 2. The first API Call returns data like so: [{ "id": 410, "name": "Test customdata test", "layer": [79,94] }, { "id": 411, "name": "Test customdata test2", ...

Angular TextInput Components don't seem to function properly when dealing with arrays

I am trying to create a collection of text input components with values stored in an array. However, when using the following code, the values seem to be placed incorrectly in the array and I cannot identify the bug. <table> <tr *ngFor="let opt ...

The intricacies of Mongoose schemas and virtual fields

I'm currently working on a NodeJS project using TypeScript along with Mongoose. However, I encountered an issue when trying to add a virtual field to my schema as per the recommendations in Mongoose's documentation. The error message stated that ...

Mastering the proper usage of the import statement - a guide to seamless integration

I'm excited to experiment with the npm package called camera-capture, which allows me to capture videos from my webcam. As someone who is new to both npm and typescript, I'm a bit unsure about how to properly test it. Here's what I've ...

Guide on incorporating the authorization function from next-auth into a TypeScript Next.js 13 app directory

Can you help me understand the proper way to declare the authorize function in [...nextauth].ts? I have been attempting it as shown below: export default NextAuth({ session: { strategy: "jwt" }, providers: ...

Tips for refreshing a React component using incremental changes retrieved from an API

I am developing a unique React application using Next.js and TypeScript, with an api-backed data set in one component that needs to be cached indefinitely. Unlike traditional examples I have found online, my component must: Fetch only the most recent 100 ...

What is the best approach for declaring helper functions and constants within nest.js?

Currently, I am delving into the world of nest.js and building an API using it. However, I have hit a roadblock when it comes to defining constants and helper functions. Like many APIs, some of my endpoints require pagination, and I want to set a default ...

How can I properly containerize an Express Gatsby application with Docker?

SOLUTION: I am currently working on a project involving an express-gatsby app that needs to be built and deployed using GitHub Actions. To deploy it on Heroku, I have learned that containerizing the app is necessary. As a result, I have created a Dockerfil ...

include the ReactToastify.css file in your project using the import statement

Error in file path C:\Users\User\Documents\GitHub\zampliasurveys_frontend\node_modules\react-toastify\dist\ReactToastify.css:1 ({"Object.":function(module,exports,require,__dirname,__filename,jest){:ro ...

When using React and Material UI, there seems to be an issue with the Popover component where calling `setAnchorEl(null)` on the onClose event does not properly

I am encountering an issue with a Popover (imported from MaterialUI) nested inside a MenuItem (also imported from MaterialUI). The open prop for the popover is set to the boolean value of anchorEl. The onClose function is supposed to handle setting anchorE ...

Interactive Bootstrap 4 button embedded within a sleek card component, complete with a dynamic click event

I am trying to create a basic card using bootstrap 4 with the following HTML code. My goal is to change the style of the card when it is clicked, but not when the buttons inside the card are clicked. Currently, clicking on the test1 button triggers both ...

Deduce the generic types of conditional return based on object property

My goal is to determine the generic type of Model for each property. Currently, everything is displaying as unknown[] instead of the desired types outlined in the comments below. playground class Model<T> { x?: T } type ArgumentType<T> = T ...

Removing a dynamic TypeScript object key was successful

In TypeScript, there is a straightforward method to clone an object: const duplicate = {...original} You can also clone and update properties like this: const updatedDuplicate = {...original, property: 'value'} For instance, consider the foll ...

Angular: The type '"periodic-background-sync"' cannot be assigned to type 'PermissionName'

I am trying to enable background sync, but I keep encountering an error when I try to enter the code. Why can't it be found? Do I need to update something? This is my code: if ('periodicSync' in worker) { const status = await navigato ...

Troubleshooting: The issue of importing Angular 2 service in @NgModule

In my Angular 2 application, I have created an ExchangeService class that is decorated with @Injectable. This service is included in the main module of my application: @NgModule({ imports: [ BrowserModule, HttpModule, FormsModu ...

Error encountered while installing node modules within an angular workspace

Currently, I am facing an issue with my workspace where the command npm install is giving me a series of errors that I cannot seem to resolve. I have tried running it as an admin, manually deleting the node_modules folder, asking for help from a senior col ...

Is it possible to assign an alternative name for the 'require' function in JavaScript?

To ensure our node module is executable and includes dependencies for requiring modules at runtime, we utilize the following syntax: const cust_namespace = <bin>_require('custom-namespace'); This allows our runtime environment to internal ...

What is the process for exporting a class and declaring middleware in TypeScript?

After creating the user class where only the get method is defined, I encountered an issue when using it in middleware. There were no errors during the call to the class, but upon running the code, a "server not found" message appeared. Surprisingly, delet ...