Extract Class Methods in TypeScript

// #types#typescript // 1 comment

In object-oriented programming (OOP), we usually define an interface and implement that interface in different classes. In TypeScript, we can do the opposite. We can implement a class and derive an interface from that class without actually defining it. Why would we ever need to do this? Well, this can be very useful in various scenarios, such as creating type-safe mocks for JavaScript classes. This ensures that the mock class implements all the methods of the original class without having to define a separate interface.

Extracting the Instance Type

The first step is to extract the instance type from the class. By default, when you retrieve the type of a class in TypeScript, you get the type of the class constructor, not the type of an instance of the class. To work with the instance type, we'll define a utility type:

type ExtractInstanceType<T> = T extends new (...args: any[]) => infer R ? R : T extends { prototype: infer P } ? P : any;

Here ExtractInstanceType<T> first tries to derive the instance type from the class constructor. If the constructor is not publicly accessible, the instance type is extracted from the prototype property of the class. This is the main difference to TypeScript's built-in utility type InstanceType<T>, which only works with the public constructor.

For example, consider the following class with a private constructor:

class SomeClass { private constructor() {} someMethod() {} } //> OK: Type is SomeClass type SomeClassInstance = ExtractInstanceType<typeof SomeClass>; //> Error: Type 'typeof SomeClass' does not satisfy the constraint 'abstract new (...args: any) => any'. type SomeClassInstanceError = InstanceType<typeof SomeClass>

Extracting Methods from the Instance Type

Once we have the instance type, we can extract its methods. To do this, we'll define two more utility types:

type ExtractMethodNames<T> = { [K in keyof T]: T[K] extends (...args: any[]) => any ? K : never }[keyof T]; type ExtractMethods<T> = Pick<T, ExtractMethodNames<T>>;

ExtractMethodNames<T> is a mapped type that iterates over all keys of T and checks if each key corresponds to a method. If it does, the key is preserved; otherwise, it's assigned the never type.

ExtractMethods<T> then uses the Pick utility type to select only those properties from T that are methods.

Now, let's use these utility types to extract the methods from SomeClassInstance:

//> Type contains methods from SomeClass type SomeClassMethods = ExtractMethods<SomeClassInstance>;

In this example, SomeClassMethods is a type that includes only the methods of an instance of SomeClass.

Implementing a Class with the Extracted Methods

Finally, let's see how to define a new class that implements the SomeClassMethods type. By making sure that a class is of type SomeClassMethods, we ensure that the class has the same methods as an instance of SomeClass.

Here's how you can do it:

//> OK: Class implements methods from SomeClass class SomeClassMock implements SomeClassMethods { someMethod() {} } //> Error: Class 'SomeClassMockError' incorrectly implements interface 'SomeClassMethods'. //> Property 'someMethod' is missing in type 'SomeClassMockError' but required in type 'SomeClassMethods' class SomeClassMockError implements SomeClassMethods { anotherMethod() {} }

In this code, SomeClassMock is a new class that implements the SomeClassMethods type. This ensures that SomeClassMock includes a someMethod function. If SomeClassMock does not include a someMethod function, or if the someMethod function in SomeClassMock does not match the one in SomeClassMethods, TypeScript will throw a compilation error.

TypeScript Playground Example

The complete example is available on TypeScript Playground to test it yourself.