Extracting Class Methods: How To Derive an Interface From a Class
Tags: typescript, javascript, tutorial, testing
Published: 2023-06-25 13:44:22

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] (https://jestjs.io/docs/es6-class-mocks#an-es6-class-example). 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:

```ts
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>`](https://www.typescriptlang.org/docs/handbook/utility-types.html#instancetypetype), which only works with the public constructor.

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

```ts
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:

```ts
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`:

```ts
//> 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:

```ts
//> 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](https://www.typescriptlang.org/play?#code/MYGwhgzhAEDKD2BbApgYXFaBvAsAKGmgAcAnASwDcwAXZaYeAOwmpIFdhr4SAKASmwBffIQhJkAWWTUAFvAAm-IfmF58AenXQAogA9WYTtDLNqYRsDrUAnkWT4bdnfpKHqASVPnLAFVvIAHh8APmgAXmgfaGR9ZEZ5GEZkAHdoHgA6TLASAHMIAC5oc2sAbQBdATDQkwAzZBJoACVoAH4m6EKomNp4mCxiEnguR2RC2vroAAVoQVapjqLGawBuB39nA04pWQUAOTAUCCDQiP6SgGljRmgAa2RreBrIss6LsujY3rTM9Oy8wuK5UqoWKc0uhSSFAmghKdweTx8ZVWeBGG1cW2kcgSx3CUzIwBuQQANGi3NssftDsdgsiNOpQgB5c6ddZkGAIFDoSAQNZODloDAQTwsbx0CJ6TYeLwWZB+OwBEaPODiLlQGnQfB00LaEiDEgspwAckVT35qoghug8ngyESQ2gEBobJq1mgsjoDFMrhM1GghrAACMWOjfUlUhksrkCotrMCY4b0ry6GbBcKzDKdXrcWnRXLAiblZzBcFNXhNKE8-QmGYTDAUDsEtAaoNEIWBdyk23zeSFDBxS4yZjewEU9yczL1aXy9AmYVzdBEGwWMZEEQQMgUIxffWsTBm0gu4L8KBuYfuRJ4ASV2uN3FqOyVYKe43cAQHeJn0osKpVFqdLruDnQU-VHKALwJTNuEtEwGF1ZBOBAV0yFXddN3vK5aBIGpDDoQ1QIgZ8LUTMt6SmQY7BIGw-TEFBn2gus2QgEwciuN11jwx9z0vG5IJIS0AzYX0SGQABHNgyGE+RWNRDii3PIcEkNY9gPw8CeIAhpkJvNCHzksCFL6ERFiGGR6k-ARvxUIA) to test it yourself.

![TypeScript Playground](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qr86ucar6v8mjj2cl4u0.png)

[<figcaption>TypeScript Playground</figcaption>](https://www.typescriptlang.org/play?#code/MYGwhgzhAEDKD2BbApgYXFaBvAsAKGmgAcAnASwDcwAXZaYeAOwmpIFdhr4SAKASmwBffIQhJkAWWTUAFvAAm-IfmF58AenXQAogA9WYTtDLNqYRsDrUAnkWT4bdnfpKHqASVPnLAFVvIAHh8APmgAXmgfaGR9ZEZ5GEZkAHdoHgA6TLASAHMIAC5oc2sAbQBdATDQkwAzZBJoACVoAH4m6EKomNp4mCxiEnguR2RC2vroAAVoQVapjqLGawBuB39nA04pWQUAOTAUCCDQiP6SgGljRmgAa2RreBrIss6LsujY3rTM9Oy8wuK5UqoWKc0uhSSFAmghKdweTx8ZVWeBGG1cW2kcgSx3CUzIwBuQQANGi3NssftDsdgsiNOpQgB5c6ddZkGAIFDoSAQNZODloDAQTwsbx0CJ6TYeLwWZB+OwBEaPODiLlQGnQfB00LaEiDEgspwAckVT35qoghug8ngyESQ2gEBobJq1mgsjoDFMrhM1GghrAACMWOjfUlUhksrkCotrMCY4b0ry6GbBcKzDKdXrcWnRXLAiblZzBcFNXhNKE8-QmGYTDAUDsEtAaoNEIWBdyk23zeSFDBxS4yZjewEU9yczL1aXy9AmYVzdBEGwWMZEEQQMgUIxffWsTBm0gu4L8KBuYfuRJ4ASV2uN3FqOyVYKe43cAQHeJn0osKpVFqdLruDnQU-VHKALwJTNuEtEwGF1ZBOBAV0yFXddN3vK5aBIGpDDoQ1QIgZ8LUTMt6SmQY7BIGw-TEFBn2gus2QgEwciuN11jwx9z0vG5IJIS0AzYX0SGQABHNgyGE+RWNRDii3PIcEkNY9gPw8CeIAhpkJvNCHzksCFL6ERFiGGR6k-ARvxUIA)

---

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](https://www.linkedin.com/in/christian-zirkel/) or [GitHub](https://github.com/zirkelc). Thanks for reading!