Interfaces In Typescript

Introduction: Interfaces and Media Types

cinema

Before I started learning Typescript, I worked on a project that involved defining and utilising "Media Types". These, essentially, were contracts used within our own repositories and in requests sent to our APIs that required data sent and received by us to follow a strict pattern. If you wanted to send us information about a cinema, the hash you sent us, or received from us had to match our definition of what a cinema was, and what its attributes had to be. Interfaces play a similar role in TypeScript, and so were quite a nice feature for me to discover.

Both media types and interfaces serve as agreements that allow one party to work together with and utilise the features of another without knowing anything about each other, by agreeing to a certain policy.

As the official TypeScript documentation puts it:

In TypeScript, interfaces fill the role of naming [structural] types, and are a powerful way of defining contracts within your code as well as contracts with code outside of your project.

Interfaces%20In%20Typescript%20f8edcb05e894405b90908d7ca151093b/paul-hanaoka-RJkKjLu8I9I-unsplash.jpg

The best analogy that I've found for this would be wall sockets. You can plug an appliance in a specific socket, which has a certain shape. The socket is the interface, while the plug is the actual class implementation.

If your toaster doesn’t implement the interface, it cannot be plugged in.

Example 1: Interfaces for an Object

Sticking to the Cinema theme, let's define our first interface- for a movie:

interface Movie {
  genre: string;
  runtime: number;
  onSale: boolean;
  title: string;
}

Interfaces are declared by using the interface keyword. We haven't described any particular movie, but the code above defines our expectations of what an object in our code should be given that type. Let's see this in action. What happens if I try and flesh out some objects representing movies after writing this, without matching the expected shape.

const BatmanBegins: Movie = {
  genre: "Thriller/Action",
};

Here I get the a message from TypeScript, warning me before I compile : Type '{ genre: string; }' is missing the following properties from type 'Movie': runtime, onSale, title .

A similar thing will happen if I try to add attributes I haven't specified in our contract:

const BatmanBegins: Movie = {
  genre: "Thriller/Action",
  isGoodMovie: true,
};

I get a warning from my editor:

Type '{ genre: string; isGoodMovie: boolean; }' is not assignable to type 'Movie'. Object literal may only specify known properties, and 'isGoodMovie' does not exist in type 'Movie'.

This is really handy, as now I'm less likely to get tripped up. These warnings are particularly useful for catching spelling errors you may otherwise have missed ( for instance if you accidentally typed onsale rather than OnSale, you would get a helpful error reminding you that 'onsale' does not exist in type 'Movie'. Did you mean to write 'onSale'?

However,right now, all of these examples actually compile and emit a JavaScript file, even though TypeScript believes there to be errors. How can this be?

What happens when you compile?

Interfaces don’t actually restrict assignable properties, instead they just provide warnings to alert you of the potential for errors/ undesired behaviour. As long as the object parameter meets the required properties, anything can be added.

These can be seen with the interface we have designed so far, code we might be concerned about will still get converted to JavaScript, but TypeScript gives us the chance to catch the problematic code and decide if it's a mistake or not.

This can be seen here, if I place the following TypeScript Code in a file named example.ts:

interface Movie {
  genre: string;
  runtime: number;
  onSale?: true;
  title: string;
}

const BatmanBegins: Movie = {
  genre: "Thriller/Action",
  runtime: 8400000,
};

When I run tsc examples.ts , I get a warning in my terminal (just as described above):

examples.ts:8:7 - error TS2741: Property 'title' is missing in type '{ genre: string; runtime: number; }' but required in type 'Movie'.

8 const BatmanBegins: Movie = {
        ~~~~~~~~~~~~

  examples.ts:5:5
    5     title: string;
          ~~~~~
    'title' is declared here.

Found 1 error.

However, the compiler still produces a JavaScript file for me, containing the following code:

var BatmanBegins = {
  genre: "Thriller/Action",
  runtime: 8400000,
};

What's happening here is that the Interface defines what a movie should be, and if any object is declared to be one, warnings are given to us that ensure the object does not conform to our expectations. However, the compiler will still produce a JavaScript file we can run.

In summary, this is what our basic interface is doing:

  1. For something to be a movie, It must have all of the attributes I said it should have.
  2. Those attributes have to have values of the right type, if a movie has the onSale attribute, but it has a value of "Yes this is on sale" the programme I've written will compain and refuse to compile the code.
  3. The Movie interface serves as a complete description of the attributes a movie can have, so I should not add additional attributes (such as isGoodMovie above) to individual objects if they are given the Movie type.

But,why does the compiler still run even when TypeScript throws errors? This is best answered by Microsoft themselves. The answer is that making these allowances eases the transition from JavaScript to TypeScript, if that is what you are doing:

This is a key scenario for migrating existing JavaScript — you rename some .js file to .ts, get some type errors, but want to keep getting compilation of it while you refactor it to remove the type errors. They are ‘warnings’ in that sense; we cannot guarantee that your program does not work just because it has type errors. — RyanCavanaugh (Development lead for the TypeScript team at Microsoft)

Side note

we can be a lot stricter, and prevent this from happening by adding --doNotEmitOnErrors when we run the compiler, or by switching this feature on in our tsconfig.ts file which (as the option's name suggests) will cause the compiler to refuse to emit a JavaScript file if there are TypeScript errors in our code.

Optional attributes

Already at this point, you might start to see where using the tool in such a strict way might be less useful than we might like. What if we want to make some of these attributes optional? If the interface was larger and more complex, having to include every attribute in every object could be problematic.

Also, what if the cinema only does sales very rarely? What would be the point be of ensuring a boolean for onSale was always specified? Surely we would be better off if we only asked for onSale to be included if onSale happened to be true? Fortunately, there is a way to define our interface to handle a case like this:

interface Movie {
  genre: string;
  runtime: number;
  onSale?: true;
  title: string;
}

Now, if we remove the onSale attribute from Batman, TypeScript will not complain. Great!

const BatmanBegins: Movie = {
  genre: "Thriller/Action",
  runtime: 8400000,
  title: "Batman Begins",
};

However, if we specify it as false, we will get an error Type 'false' is not assignable to type 'true | undefined' . We can only provide the attribute for cases where onSale is true. This is exactly what we wanted.

const BatmanBegins: Movie = {
  genre: "Thriller/Action",
  runtime: 8400000,
  onSale: false,
  title: "Batman Begins",
};

The advantage of having this capability is that we can describe properties that may not be provided. while still also preventing use of properties that are not part of the interface.

Why Do We Do This?

Interfaces are useful because they provide contracts that objects can use to work together without needing to know anything else about each other. However, why do things this way?

Two reasons that come to my mind for using Interfaces are:

Cleaner, more consistent code

This is just as true as another method might be, but none the less, the principal goal when using this tool is to help prevent you from making mistakes, and to enforce certain standards. Every implementation of an interface is guaranteed to match your expectations of what the object or class in question should contain. Stricter typing and structuring will make your code less buggy.

Having a definition for the structure of an object in our codebase

Another aspect of this technique that can be useful is that you can look up interfaces within your codebase, and use them to provide you with a taxonomy of objects and classes to help you get up to speed when exploring a new library or project.

It's possible to have your interfaces in their own directory, and import them in files when they are used. This way, someone new to your project could quickly get a sense of how the OO parts of your projects click together, and understand many of the conventions that are in place within your codebase.

Class implementation

Now that we have seen how interfaces are implemented with objects in TypeScript, let's take a look at how they work with Classes.

Let's say our cinema chain is doing a promotion, and is going to bring in some lookalikes to act like some famous actors, (and let's assume for some unknown reason they decided to write this up in TypeScript).

To be one of those actors, you have to be able to recite their catchphrase. This is defined, as before in the interface

interface Actor {
  CatchPhrase(): string;
}

To fulfil the role, you have to perform a function catchPhrase that returns a string.

class MatthewMcConaugheyClone implements Actor {}

Here, this class is declaring itself to implement our interface above. However it doesn't fulfil the requirements! Upon being asked to compile, TypeScript informs me that Class 'MatthewMcConaughey' incorrectly implements interface 'Actor'. Property 'CatchPhrase' is missing in type 'MatthewMcConaughey' but required in type 'Actor'.

Well... Alright, Alright, Alright!

class MatthewMcConaugheyClone implements Actor
    CatchPhrase() {
        return "Alright, Alright, Alright!";
    }
}

Much better, the class now obeys the rules laid out in the interface. Let's make one more Class so I can show you how all this works with variables:

class ArnoldSchwarzeneggerClone implements Actor {
  CatchPhrase() {
    return "I'll be back!";
  }
}

Variable Declarations

If we declare two new variables, and say that they have to point to something that fulfils the Actor interface, we can do the following way.

let HasPhrase1: Actor;
let HasPhrase2: Actor;

HasPhrase1 = new MatthewMcConaugheyClone();
HasPhrase2 = new ArnoldSchwarzeneggerClone();

This is pretty useful, as we can describe & guarentee some kind of expected behaviour from HasPhrase1 and HasPhrase2 without knowing anything about what they are being assigned to ( a lot like our USB sticks from earlier). All we know is that they will have a catchPhrase method.

Finally, We can also declare a variable that will only point to an array of entities that implement the Actor interface we created like so:

let PhraseBook: Actor[];

PhraseBook = [HasPhrase1, HasPhrase2];

This is handy because we know that everything in this array will satisfy certain requirements, they will all have the CatchPhrase method. So, if we wanted to iterate through this array, we can do so with a much greater sense of confidence than we could have done without the interface logic being implemented, as would be the case in JavaScript.

Conclusion

Hopefully you have some sense now of the following

  1. What interfaces are in Typescript, and the role they play in the language.
  2. How Interfaces are used to define the structure of Objects in TypeScript.
  3. How interfaces are used to make classes conform to standards and exhibit predictable behaviour.
  4. How variables can be assigned to point to instances of a class, or an array of instances, that satisfy an interfaces requirements.

Good Luck and thanks for reading!