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:

 

image


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.

 

image

Published Tuesday, January 8, 2013 6:09 PM by dwahlin
Filed under: , ,

Comments

# re: The Role of Interfaces in TypeScript

Wednesday, January 9, 2013 8:13 PM by Lars Klint

Great concise post Dan. And interfaces don't "cost" much up front, but saves you so much time as you get deeper into the world of JS in your project.

Is it worth mentioning that if you do have optional interface properties, the implementation need to check that the property is there before using it? Otherwise you might run into trouble.

# re: The Role of Interfaces in TypeScript

Thursday, January 10, 2013 4:15 AM by Elad Katz

Good post!

Since an object can implement an interface without inheriting from it (duck typing etc.) is it possible to define IjQueryInterface that will have all of the signature of all of the methods in jQuery and then use this interface to get full intellisense and metadata over the *regular* jQuery lib?

That would be just awesome, but I can't figure out the syntax for that.