DEV Community

Chris Cook
Chris Cook

Posted on • Originally published at zirkelc.dev

Extracting Class Methods: How To Derive an Interface From a Class

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;
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>>;
Enter fullscreen mode Exit fullscreen mode

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>; 
Enter fullscreen mode Exit fullscreen mode

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() {}
}
Enter fullscreen mode Exit fullscreen mode

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.

TypeScript Playground

TypeScript Playground


I hope you found this post helpful. If you have any questions or comments, feel free to leave them below. If you'd like to connect with me, you can find me on LinkedIn or GitHub. Thanks for reading!

Top comments (1)

Collapse
 
niklampe profile image
Nik

Very interesting!