Interfaces

Let's revisit our discussion of interfaces for a moment and look at how they interact with classes. In the next example, we will enforce the IPoint interface upon the Point class. Classes can optionally inherit type information from interfaces using the implements keyword. The class will then be required to implement all of the interface members; otherwise, compile errors will occur:

interface IPoint {
    x: number;
    y: number;
}
class Point implements IPoint {
    constructor(public x: number, public y = 0) {
    }
}

As we discussed earlier, interfaces are a purely compile time construct. The JavaScript that is output from this example is completely identical to the JavaScript we just saw. I snuck in a shorthand method of defining instance variables on classes too. Decorating the constructor's parameters with the public or private keywords tells TypeScript to treat these objects as part of the type and not just initialization parameters.

Classes are not limited to implementing a single interface. Providing a comma separated list of interfaces after the implements keyword allows your class to provide implementations of a variety of different contracts. Let's make our Point class more useful by implementing a second interface, as follows:

interface IPoint {
    x: number;
    y: number;
}
interface ICompare {
    Compare(p2: IPoint): number;
}
class Point implements IPoint, ICompare {
    public x: number;
    public y: number;
    constructor(x: number, y = 0) {
        this.x = x;
        this.y = y;
    }
    public Compare(p2: IPoint): number {
        var p1Val = this.x * this.x + this.y * this.y;
        var p2Val = p2.x * p2.x + p2.y * p2.y;
        var result = p1Val - p2Val;
        if (result == 0) {
            return 0;
        } else if (result > 0) {
            return 1;
        } else {
            return -1;
        }
    }
}

The ability to enforce multiple interfaces on our classes provides us with the ability to use our objects in several different contexts. Keeping our interfaces simple allows them to be more reusable across our applications. We could have easily placed the Compare method on the IPoint interface and achieved the same result. However, as we will see later with a few tweaks, any number of types could implement the ICompare interface, which have no need for the members x and y. In this case, the Compare method determines the distance each point is from the origin and returns a number representing which one is farthest away. If the object performing the comparison is farther away, a positive value is returned. If the point that has been passed as a parameter is farther away then a negative value is returned. If the two points are equivalent distances from the origin then a value of zero is returned.

Static and instance members

Both x and y are instance members of the class Point; this means that an object of the type Point must be instantiated for them to be referenced. When the constructor is called, values are assigned to properties of the object instance being created. These values can either be new objects created within the constructor function or references to objects passed in as the constructor parameters. This is a good thing for certain members, however, there are other class members that you may want access to even when an object has not been created. TypeScript provides this ability through static members. Static members are accessible at any time by referencing the named type. To declare a static member you simply decorate it with the static keyword:

class Point implements IPoint, ICompare {
    public x: number;
    public y: number;
    constructor(x: number, y = 0) {
        this.x = x;
        this.y = y;
    }
    public Compare(p2: IPoint): number {
        var p1Val = this.x * this.x + this.y * this.y;
        var p2Val = p2.x * p2.x + p2.y * p2.y;
        var result = p1Val - p2Val;
        if (result == 0) {
            return 0;
        } else if (result > 0) {
            return 1;
        } else {
            return -1;
        }
    }
    static Compare(p1: Point, p2: Point): number {
        return p1.Compare(p2);
    }
}

The preceding Compare method takes in two Point objects and compares their distance from the origin to determine which is greater. The JavaScript output for this class member is very different from that of the x and y properties being created in the constructor. The following output shows that the Compare method is put directly on the Point object being created:

var Point = (function () {
    function Point(x, y) {
        if (typeof y === "undefined") { y = 0; }
        this.x = x;
        this.y = y;
    }
    Point.prototype.Compare = function (p2) {
        var p1Val = this.x * this.x + this.y * this.y;
        var p2Val = p2.x * p2.x + p2.y * p2.y;
        var result = p1Val - p2Val;
        if (result == 0) {
            return 0;
        } else if (result > 0) {
            return 1;
        } else {
            return -1;
        }
    };
    Point.Compare = function (p1, p2) {
        return p1.Compare(p2);
    };
    return Point;
})();

We still haven't discussed one of the most important parts of TypeScript classes, and that's instance member functions. These functions are only available to an instance of the object of the type and they have access to the this object that allows easy access to all of an object's members. In JavaScript, there are a couple of ways to create instance member functions. They can be added directly to the object using the this object during the object's creation, or they can be attached to the object's prototype. There are advantages and disadvantages to both, but it is generally better to put functions on the prototype rather than assign them to each new instance of the class. The instance of the function placed on the static prototype is then reused over and over rather than being recreated with each new instance of the type. This helps reduce memory consumption and will improve performance as your applications grow. TypeScript has adopted this method for attaching methods to instances of an object.

Let's create a new type that represents an object on a plane. It will have three properties: Height, Width, and Location. There will also be one instance member function that determines the size of the object on the plane. The Location member will be of type Point that we created just a little while ago:

interface IBounds {
    Location: IPoint;
    Height: number;
    Width: number;
    Size(): number;
}
class Bounds implements IBounds {
    public Location: IPoint = new Point(0, 0);
    public Height: number = 0;
    public Width: number = 0;
    public Size() {
        return this.Height * this.Width;
    }
}

As you can see, this class does not contain an explicit constructor; however, all of its properties are initialized and will be available for consumption when an instance of the type is created. As you can see from the resulting JavaScript, the Size method is placed on the Bounds object's static prototype member and a parameter-less constructor is created where all of the instance members are initialized:

var Bounds = (function () {
    function Bounds() {
        this.Location = new Point(0, 0);
        this.Height = 0;
        this.Width = 0;
    }
    Bounds.prototype.Size = function () {
        return this.Height * this.Width;
    };
    return Bounds;
})();

Properties

In ECMAScript 5, the concept of properties was introduced. This allows a developer to create getter and setter methods for a designated instance method that can be accessed like properties. If you have your TypeScript project set to compile ECMAScript 5, then you will be able to use the get and set keywords to define properties according to the ECMAScript 5 language specification. The ECMAScript version can be changed in the General settings of the TypeScript Build section of the project properties as shown in the following screenshot:

Properties

Let's modify our bounds class to treat both Height and Width as object properties rather than simple instance members and create both getter and setter methods for them both. The getters and setters will modify private instance members too, which will maintain the actual value. It is common practice in JavaScript to name private instance members with a leading underscore, so we will create _height and _width instance members for the Bounds class to access internally.

Since we now have a method block in which we can execute some code we should also provide some value checking to ensure correct program execution. The height and width of an object should never be less than zero so let's verify that the values being assigned to them fit this constraint; if not, we will set the value to zero manually:

interface IBounds {
    Location: IPoint;
    Height: number;
    Width: number;
    Size(): number;
}
class Bounds implements IBounds {
    public Location: IPoint = new Point(0, 0);
    private _height: number = 0;
    private _width: number = 0;
    public Size() {
        return this.Height * this.Width;
    }

    get Height(): number {
        return this._height;
    }
    set Height(value: number) {
        this._height = (value > 0) ? value : 0;
    }

    get Width(): number {
        return this._width;
    }
    set Width(value: number) {
        this._width = (value > 0) ? value : 0;
    }
}

As you can see, we now have method blocks wrapping the access of the Height and Width properties. The properties are still accessed as if they were instance variables as you can see from the Size method, however, our function blocks are executed when we access them. The resulting JavaScript calls the Object.defineProperty method, outlined in the ECMAScript 5 standard, when the type is created.

Note

While the ECMAScript 5 standard has been widely adopted by most browsers, it is important to note that there are some lapses in feature support in certain browsers. The table found at the following link outlines the current support for the ECMAScript 5 standard: http://kangax.github.io/compat-table/es5/.

This places a new object on the prototype that is accessible on all instances of that type. The following JavaScript is what is generated by defining getters and setters in TypeScript:

var Bounds = (function () {
    function Bounds() {
        this.Location = new Point(0, 0);
        this._height = 0;
        this._width = 0;
    }
    Bounds.prototype.Size = function () {
        return this.Height * this.Width;
    };

    Object.defineProperty(Bounds.prototype, "Height", {
        get: function () {
            return this._height;
        },
        set: function (value) {
            this._height = (value > 0) ? value : 0;
        },
        enumerable: true,
        configurable: true
    });

    Object.defineProperty(Bounds.prototype, "Width", {
        get: function () {
            return this._width;
        },
        set: function (value) {
            this._width = (value > 0) ? value : 0;
        },
        enumerable: true,
        configurable: true
    });
    return Bounds;
})();

There are a few other notables from this output to consider. We created new private variables to store the values being surfaced by our property declarations. These private variables are being added to each instance of the object in the constructor. This seems a little strange since we know that closures can store a private state for each instance of the class. The reason that private variables need to be added to each instance is because the functions and properties are attached to the prototype on the class. The prototype does not retain the state of the closure and relies on the actual object instance to access its members.

The IBounds interface is still upheld after converting to properties as well. The resulting type information from creating an instance of the class just has to match that of the interface for compilation. There is no distinction made between whether it is an instance member or a property declaration because they are both accessed in the same manner.

We have covered quite a bit here in a very short period of time. Classes are one of the most important concepts introduced by TypeScript that help turn JavaScript into an enterprise-level development platform. We still have a number of concepts to cover, including enums.

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

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