Interfaces in TypeScript
Interfaces In Typescript
Introduction: Interfaces and Media Types
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.
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:
- For something to be a movie, It must have all of the attributes I said it should have.
- 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. - 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 theMovie
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
- What interfaces are in Typescript, and the role they play in the language.
- How Interfaces are used to define the structure of Objects in TypeScript.
- How interfaces are used to make classes conform to standards and exhibit predictable behaviour.
- 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!