Interfaces in TypeScript

As applications scale and more classes and constructs are created, we need to find ways to ensure consistency and rules compliance in our code. One of the best ways to address the consistency and type validation issue is to create interfaces.

In a nutshell, an interface is a code blueprint defining a certain field's schema and any types (either classes, function signatures) implementing these interfaces are meant to comply with this schema. This becomes quite useful when we want to enforce strict typing on classes generated by factories, when we define function signatures to ensure that a certain typed property is found in the payload, or other situations.

Let's get down to business! Here, we define the Vehicle interface. Vehicle is not a class, but a contractual schema that any class that implements it must comply with:

interface Vehicle {
make: string;
}

Any class implementing the Vehicle interface must feature a member named make, which must be typed as a string according to this example. Otherwise, the TypeScript compiler will complain:

class Car implements Vehicle {
// Compiler will raise a warning if 'make' is not defined
make: string;
}

Interfaces are therefore extremely useful to define the minimum set of members any type must fulfill, becoming an invaluable method for ensuring consistency throughout our codebase.

It is important to note that interfaces are not used just to define minimum class schemas, but any type out there. This way, we can harness the power of interfaces for enforcing the existence of certain fields and methods in classes and properties in objects used later on as function parameters, function types, types contained in specific arrays, and even variables. An interface may contain optional members as well and even members.

Let's create an example. To do so, we will prefix all our interface types with an I (uppercase). This way, it will be easier to find its type when referencing them with our IDE code autocompletion functionality.

First, we define an Exception interface that models a type with a mandatory message property member and an optional id member:

interface Exception {
message: string;
id?: number;
}

We can define interfaces for array elements as well. To do so, we must define an interface with a sole member, defining index as either a number or string (for dictionary collections) and then the type what we want that array to contain. In this case, we want to create an interface for arrays containing Exception types. This is a type comprising of a string message property and an optional ID number member, as we said in the previous example:

interface ExceptionArrayItem {
[index: number]: IException;
}

Now, we define the blueprint for our future class, with a typed array and a method with its returning type defined as well:

interface ErrorHandler {
exception: ExceptionArrayItem[];
logException(message: string; id?: number: void;)
}

We can also define interfaces for standalone object types. This is quite useful when it comes to defining a templated constructor or method signatures, which we will see later in this example:

interface ExceptionHandlerSettings {
logAllExceptions: boolean;
}

Last but not least, in the following class, we will implement all these interface types:

class ErrorHandler implements ErrorHandler {
exceptions: ExceptionArrayItem[];
logAllExceptions: boolean;
constructor(settings: ExceptionHandlerSettings) {
this.logAllExceptions = settings.logAllExceptions;
}

logException(message: string, id?: number): void {
this.exception.push({ message, id });
}
}

Basically, we are defining an error handler class here that will manage an internal array of exceptions and expose a method to log new exceptions by saving them into the aforementioned array. These two elements are defined by the ErrorHandler interface and are mandatory. The class constructor expects the parameters defined by the ExceptionHandlerSettings interface and uses them to populate the exception member with items typed as Exception. Instancing the ErrorHandler class without the logAllExceptions parameter in the payload will trigger an error.

So far, I've been explaining interfaces as we are used to seeing them in other high level languages, but interfaces in TypeScript are on steroids; let me exemplify that by using the following code:

interface A {
a
}

var instance = <A>{ a: 3 };
instance.a = 5;

Here, we declare an interface, but we also create an instance from an interface here:

var instance = <A>{ a: 3 };

This is interesting because there are no classes involved here. That means writing a mocking library is a piece of cake. Let's explain a bit what we mean with a mock library. When you are developing code you might think in interfaces before your start thinking in concrete classes. This is because you know what methods needs to exist but you might not have decided exactly how the methods should carry out a task. Imagine that you are building an order module. You have logic in your order module and you know that you at some point need to talk to a database service that will help you persist your order. You come up with a contract for said database service, an interface. You defer implementation of said interface until later. At this point a mocking library come in and is able to create a mock instance from an interface. Your code at this point might looking something like this:

class OrderProcessor {
constructor(private databaseService: DatabaseService) {}

process(order) {
this.databaseService.save(order);
}
}

interface DatabaseService {
}

let orderProcessor = new OrderProcessor(mockLibrary.mock<DatabaseService>());
orderProcessor.process(new Order());

So mocking at this point gives us the ability to defer implementation of DatabaseService until we are done writing the OrderProcessor. It also makes the test experience of OrderProcessor a whole lot better. Where we in other languages needed to bring in mock library as 3rd party dependency we can now utilize a built in construct in TypeScript by typing the following:

var databaseServiceInstance = <DatabaseService>{};

This will give us an instance of DatabaseService. A word of warning though, you are responsible for adding a process() method to your instance. Your instance starts out as an empty object.

This would not raise any problems with the compiler; this means that it is a powerful feature, but it leaves it to you to verify that what you create is correct.

Let's emphasize how powerful this TypeScript feature really is by looking at some more code cases, where it pays off to be able to mock away things. Let's reiterate that the reason for mocking anything in your code is to make it easier to test.

Assume your code looks something like this:

class Stuff {
srv:AuthService = new AuthService();
execute() {
if (srv.isAuthenticated()) // do x
else // do y
}
}

A better way to test this is to make sure that the Stuff class relies on abstractions, which means that the AuthService should be created elsewhere and that we talk to an interface of AuthService rather than the concrete implementation. So, we would modify our code to look like this:

interface AuthService {
isAuthenticated(): boolean;
}

class Stuff {
constructor(srv:AuthService) {}
execute() {
if (srv.isAuthenticated()) { /* do x */ }
else { /* do y */ }
}
}

To test this class, we would normally need to create a concrete implementation of AuthService and use that as a parameter in the Stuff instance, like this:

class MockAuthService implements AuthService {
isAuthenticated() { return true; }
}
var srv = new AuthService();
var stuff = new Stuff(srv);

It would, however, become quite tedious to have to write a mock version of every dependency that you wanted to mock away. Therefore, mocking frameworks exist in most languages. The idea is to give the mocking framework an interface that it would create a concrete object from. You would never have to create a mock class, as we did previously, but that would be something that would be up to the mocking framework to do internally. Using said mock framework it would look something like this:

var instance = mock<Type>();

We have already stated so far how easy it is to create an instance from an interface, like so:

var instance = <A>{ a: 3 };

This means creating a mocking framework is then as easy as typing the following:

function mock<T>(startData) {
return <T>Object.assign({}, startData);
}

And using it in the following way:

interface IPoint {
x;
y;
}

class Point implements IPoint {
x;
y;
}
var point = mock<IPoint>({ x: 3 });
console.log(point);

Let's wrap up this section about interfaces by highlighting that classes can implement more than one interface, but also that interfaces are supercharged and facilitates testing quite a lot.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset