Deriving interfaces from classes in TypeScript

August 18, 2019 0 Comments

Deriving interfaces from classes in TypeScript

 

 

Most object oriented programming languages encourage the pattern of Programming to an interface. TypeScript supports this of course, you can create one or more interfaces, and then define classes (or factories) that generate instances of this interface.

However, as the linked article above points out, programming to an interface is sometimes overused when the programmer anticipates the possibility of multiple concrete implementations in future although there may exist just one at the time of implementation.

But, until these multiple implementations are actually required, these single-implementation interfaces continue to increase maintenance overhead, because every time we need to introduce a new member, we need to modify two places. Sure, the tooling helps with this but it still not ideal.

Also, relying on just the concrete implementation is not ideal because TypeScript compiler service doesn’t yet have a good mechanism to bulk-replace all usage of a concrete implementation with the corresponding interface should we need multiple implementations in future.

So, in this post we explore two features of typescript that may help us with this.

A lesser known feature of TypeScript is that interfaces can be derived from classes.

It may seem like a no-brainer that this is a solution for our use case. But there is an important caveat: All private/protected members of the implementation are exposed in the derived interface.

If you are surprised by this, you wouldn’t be alone. I found it highly counter-intuitive when I encountered it first, but the rationale behind this is explained in the official docs:

When an interface type extends a class type it inherits the members of the class but not their implementations. It is as if the interface had declared all of the members of the class without providing an implementation. Interfaces inherit even the private and protected members of a base class. This means that when you create an interface that extends a class with private or protected members, that interface type can only be implemented by that class or a subclass of it.

This is useful when you have a large inheritance hierarchy, but want to specify that your code works with only subclasses that have certain properties. The subclasses don’t have to be related besides inheriting from the base class.

So, that is all good, but if we want just the public members, what do we do ?
Thankfully a simple solution exists:

We can use mapped types derived from public members of a type.

Since a class definition is implicitly a type definition, this works well for classes too:

Now SyncBackend type will have only public members of FSSyncBackend class.

While for most uses this solves our purpose, but if we strictly need an interface & not an alias (perhaps to improve type error messages), we can simply define an interface that extends from this alias:

Eventually if we do need multiple implementations, we can choose to either have an explicitly defined interface or keep one implementation as the canonical implementation, derive the interface from it and make the other implementations comply to that.

I usually resort to the latter when the other implementations are not for public consumption and exist just for stubbing in test cases.

If multiple implementations are actually exposed then the former solution (explicitly defined interface) helps with separating the API documentation of the interface from the documentation of individual implementations.


Tag cloud