The Role of Interfaces in TypeScript
In my last post I talked about how classes and interfaces could be extended in the TypeScript language. By using TypeScript’s extends keyword you can easily create derived classes that inherit functionality from a base class. You can also use the extends keyword to extend existing interfaces and create new ones. In the previous post I showed an example of an ITruckOptions interface that extends IAutoOptions. An example of the interfaces is shown next:
interface IAutoOptions { engine: IEngine; basePrice: number; state: string; make: string; model: string; year: number; } interface ITruckOptions extends IAutoOptions { bedLength: string; fourByFour: bool; }
I also showed how a class named Engine can implement an interface named IEngine. By having the IEngine interface in an application you can enforce consistency across multiple engine classes.
interface IEngine { start(callback: (startStatus: bool, engineType: string) => void) : void; stop(callback: (stopStatus: bool, engineType: string) => void) : void; } class Engine implements IEngine { constructor(public horsePower: number, public engineType: string) { } start(callback: (startStatus: bool, engineType: string) => void) : void { window.setTimeout(() => { callback(true, this.engineType); }, 1000); } stop(callback: (stopStatus: bool, engineType: string) => void) : void { window.setTimeout(() => { callback(true, this.engineType); }, 1000); } }
Although interfaces work well in object-oriented languages, JavaScript doesn’t provide any built-in support for interfaces so what role do they actually play in a TypeScript application? The first answer to that question was discussed earlier and relates to consistency. Classes that implement an interface must implement all of the required members (note that TypeScript interfaces also support optional members as well). This makes it easy to enforce consistency across multiple TypeScript classes. If a class doesn’t implement an interface properly then the TypeScript compiler will throw an error and no JavaScript will be output. This lets you catch issues upfront rather than after the fact which is definitely beneficial and something that simplifies maintenance down the road.
If you look at the JavaScript code that’s generated you won’t see interfaces used at all though – JavaScript simply doesn’t support them. Here’s an example of the JavaScript code generated by the TypeScript compiler for Engine:
var Engine = (function () { function Engine(horsePower, engineType) { this.horsePower = horsePower; this.engineType = engineType; } Engine.prototype.start = function (callback) { var _this = this; window.setTimeout(function () { callback(true, _this.engineType); }, 1000); }; Engine.prototype.stop = function (callback) { var _this = this; window.setTimeout(function () { callback(true, _this.engineType); }, 1000); }; return Engine; })();
Looking through the code you’ll see that there’s no reference to the IEngine interface at all which is an important point to understand with TypeScript – interfaces are only used when you’re writing code (the editor can show you errors) and when you compile. They’re not used at all in the generated JavaScript.
In addition to driving consistency across TypeScript classes, interfaces can also be used to ensure proper values are being passed into properties, constructors, or functions. Have you ever passed an object literal (for example { firstName:’John’, lastName:’Doe’}) into a JavaScript function or object constructor only to realize later that you accidentally left out a property that should’ve been included? That’s an easy mistake to make in JavaScript since there’s no indication that you forgot something. With TypeScript that type of problem is easy to solve by adding an interface into the mix.
The Auto class shown next demonstrates how an interface named IAutoOptions can be defined on a constructor parameter. If you pass an object into the constructor that doesn’t satisfy the IAutoOptions interface then you’ll see an error in editors such as Visual Studio and the code won’t compile using the TypeScript compiler.
class Auto { basePrice: number; engine: IEngine; state: string; make: string; model: string; year: number; constructor(options: IAutoOptions) { this.engine = options.engine; this.basePrice = options.basePrice; this.state = options.state; this.make = options.make; this.model = options.model; this.year = options.year; } }
An example of using the Auto class’s constructor is shown next. In this example the year (a required field in the interface) is missing so the object doesn’t satisfy the IAutoOptions interface.
var auto = new Auto({ engine: new Engine(250, 'V8'), basePrice: 45000, state: 'Arizona', make: 'Ford', model: 'F-150' });
In this example the object being passed into the Auto’s constructor implements 5 out of 6 fields from the IAutoOptions interface. Because the constructor parameter requires 6 fields an error will be displayed in the editor and the TypeScript compiler will error out as well if you try to compile the code to JavaScript. An example of the error displayed in Visual Studio is shown next:
This makes it much easier to catch issues such as missing data while you’re writing the initial code as opposed to catching issues after the fact while trying to run the actual JavaScript. If you write unit tests then this functionality should also help ensure that tests are using proper data.
Interfaces also allow for more loosely coupled applications as well. Looking back at the Auto class code you’ll notice that the engine field is of type IEngine. This allows any object that implements the interface to be passed which provides additional flexibility. The same can be said about the Auto’s constructor parameter since any object that implement IAutoOptions can be passed.
Conclusion
In this post you’ve seen that Interfaces provide a great way to enforce consistency across objects which is useful in a variety of scenarios. In addition to consistency, interfaces can also be used to ensure that proper data is passed to properties, constructors and functions. Finally, interfaces also provide additional flexibility in an application and make it more loosely coupled. Although they never appear in the generated JavaScript, they help to identify issues upfront as you’re building an application and certainly help as modifications are made in the future.
If you’d like to learn more about TypeScript check out the TypeScript Fundamentals course on Pluralsight.com.