What is the process for defining a type that retrieves all functions from a TypeScript class?

Imagine having a class called Foo

class Foo {
  bar(){
     // do something
  }

  baz() {
    // do something
  }
}

How can you define a type ExtractMethods that takes a class and returns an interface or type containing the class methods?

For example:

type FooMethod = ExtractMethods<Foo> // => { bar(): void; baz(): void; } 

One common response to this question might be "Why not just create an interface for Foo and use it?"

However, in the scenario I need to address, Foo is not a class I personally created, but rather a third-party class. Creating an interface for Foo may not accurately reflect its methods, especially if the class undergoes changes in future updates. This is what I aim to avoid by seeking another solution.

Answer №1

Let's create a custom function called ExtractMatchingProps<T, V> that receives an object type T and a property value type V, returning a new object type with only those properties of T whose types match or are assignable to V. We can achieve this easily by utilizing key remapping in mapped types:

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

When we map a key to never, we essentially exclude that property from the resulting type. Subsequently, by calling ExtractMethods<T> and passing it an object of type T, we effectively extract all method-type properties from T:

type ExtractMethods<T> = ExtractMatchingProps<T, Function>;;

The output for this operation would look like:

class Foo {
    bar() { }
    baz() { }
    notAMethod = 123;
    funcProp = () => 10;
}

type FooMethod = ExtractMethods<Foo>
/* type FooMethod = {
    bar: () => void;
    baz: () => void;
    funcProp: () => number;
} */

As observed, the property notAMethod with a type of number is excluded from FooMethod, while methods like bar() and baz() are retained as intended. Essentially, the function-typed property funcProp is also preserved in FooMethod. It's important to note here that the type system may not reliably differentiate between a method and a function-valued property, treating both similarly due to them being present on either the instance directly or the class prototype.

This illustrates the best way to implement ExtractMethods in a generic manner.


Alternatively, if you wish to target this functionality specifically for a particular class such as Foo and don't mind moving from the type level to the value level, you can leverage the fact that spreading an instance of a class into a new object only includes instance properties:

const spreadFoo = { ... new Foo() };
/* const spreadFoo: {
  notAMethod: number;
  funcProp: () => number;
} */

From this object, you could then derive FooMethod (assuming there are no non-method prototype members):

type FooMethod = Omit<Foo, keyof typeof spreadFoo>;
/* type FooMethod = {
  bar: () => void;
  baz: () => void;
} */

However, the practicality of this approach largely depends on your specific use cases.

Link to Playground for code demonstration

Answer №2

After reading through an informative article, I devised a solution which you can test out in this interactive playground:

class Test {
  foo() {}
  bar() {}
  zar = 3;
}

type SubType<Base, Condition> = Pick<Base, {
    [Key in keyof Base]: Base[Key] extends Condition ? Key : never
}[keyof Base]>;

type MethodsOnly<T> = SubType<T, () => unknown>;

type T = Pick<Test, keyof MethodsOnly<Test>>

const test = {} as T;

test.bar; // valid
test.foo; // valid
test.zar; // Property 'zar' does not exist on type 'T'.(2339)

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

Validation of object with incorrect child fields using Typeguard

This code snippet validates the 'Discharge' object by checking if it contains the correct children fields. interface DischargeEntry { date: string; criteria: string; } const isDischargeEntry = (discharge:unknown): discharge is DischargeEntry ...

The component is failing to store its value within the database

I'm encountering an problem when attempting to save an option in the database. To address this issue, I created a component in Svelte called StatePicker that is responsible for saving US States. However, when I try to save it in the database using a ...

Angular with NX has encountered a project extension that has an invalid name

I am currently using Angular in conjunction with nx. Whenever I attempt to execute the command nx serve todos, I encounter the following error: Project extension with invalid name found The project I am working on is named: todos. To create the todos app ...

mobx: invoking a class method upon data alteration

Is it possible to utilize the Mobx library in order to trigger a class method whenever data changes? For instance, when MyObject assigns a value of 10 to container['item'], can we have the myaction method invoked? class MyElement extends Compone ...

Setting an optional property to null is not permitted

In my model class, I have defined an optional property as follows: export class Workflow { constructor( public id: number, public started: Date, public documentId: number, public document: Document, public status: WorkflowStatus, ...

Angular HTTP client implementation with retry logic using alternative access token

Dealing with access tokens and refresh tokens for multiple APIs can be tricky. The challenge arises when an access token expires and needs to be updated without disrupting the functionality of the application. The current solution involves manually updati ...

How can I retrieve the value of an array using ngForm in Angular 2?

One concrete example is a component I created that looks like this. @Component({ selector: 'home', template: ` <form (ngSubmit)="onSubmit(f)" #f="ngForm"> <input type="text" ngControl="people"> ...

Having trouble with SVG Circles - Attempting to create a Speedometer design

Trying to implement a speedometer in one of the components of my Vue project, but encountering an issue. When I input 0 into my progress calculation for determining the stroke fill, it overlaps with the background circle instead of staying within its bound ...

Unable to utilize Stats.js with @angular/cli version 1.4.4

Attempting to utilize @types/stats with @angular/cli following the guidance at https://github.com/angular/angular-cli/wiki/stories-third-party-lib. However, encountering a tslint error when trying to import * as STATS from 'stats.js'. [ts] Modul ...

The specified property is not found in the type 'IntrinsicAttributes & IntrinsicClassAttributes<DatePicker> & Readonly<{ children?: ReactNode; }>'

As I delve into utilizing React along with TypeScript and Material-UI components, I encounter some errors. One such error message pops up like this: The Property 'openToYearSelection' is not found on type 'IntrinsicAttributes & Intr ...

No output when using Typescript 2.0

Recently, I've been working on a project in VS 2015 update 3 and just integrated Typescript 2.0. Initially, I encountered a lot of errors and had to go through a trial and error process to resolve them. Now, all the errors have been fixed but I' ...

Storing Json data in a variable within Angular 2: a step-by-step guide

https://i.sstatic.net/2QjkJ.png Within the params.value object, there are 3 arrays containing names that I need to extract and store in a variable. I attempted to use a ForEach loop for this purpose, but encountered an issue. Can you spot what's wron ...

Issue with Angular: ngForm object does not capture selected option

Revise to clean up unnecessary code. Having trouble displaying the selected option when I print the form object to the console. It's showing as undefined. Any guidance on what might be wrong with this code would be appreciated. Let me know if more in ...

Converting objects to arrays in Typescript: A step-by-step guide

Can anyone assist me in converting a string to a Typescript array? Any help would be greatly appreciated. Take a look at the following code snippet: private validateEmptyOption(): any { console.log("CHECKED") let isValid = true; this.currentF ...

Instructions on how to dynamically show specific text within a reusable component by utilizing React and JavaScript

My goal is to conditionally display text in a reusable component using React and JavaScript. I have a Bar component that I use in multiple other components. In one particular ParentComponent, the requirement is to show limit/total instead of percentage va ...

Prevent ESLint from linting files with non-standard extensions

My .estintrc.yaml: parser: "@typescript-eslint/parser" parserOptions: sourceType: module project: tsconfig.json tsconfigRootDir: ./ env: es6: true browser: true node: true mocha: true plugins: - "@typescript-eslint" D ...

What is the most efficient way to execute useEffect when only one specific dependency changes among multiple dependencies?

My main objective is to update a state array only when a specific state (loadingStatus) undergoes a change. Yet, if I include solely loadingStatus as a dependency, React throws an error requesting all dependencies [loadingStatus, message, messageArray, set ...

Remove the Prisma self-referencing relationship (one-to-many)

I'm working with this particular prisma schema: model Directory { id String @id @default(cuid()) name String? parentDirectoryId String? userId String parentDirectory Directory? @relation("p ...

Using the as operator in TypeScript for type casting a string

function doSomething(a : any) { let b = (a as Array<any>) alert(typeof b) // displays "string" } doSomething("Hello") The alert is showing "string" instead of what I anticipated, which was something along the lines of a null value. The docu ...

What is the best way to create a Typescript type consisting of only the public members of a different type?

Inside the realm of Typescript 4.3.5 In what manner can I establish a type that consists solely of the public members and properties of another type? Take into account: class Thing { public name: string private secret: string public greet(): string ...