TypeScript

We revert here to the study of TypeScript that we started in Chapter 04 Comparing Approaches to Programming, and that served as an introduction to the language, the tools to use, its integration with Visual Studio and a basic coverage of its possibilities.

In that chapter, I promised to review the characteristics of the language since it is the other big Microsoft project related to open source since its inception, and it's just gaining momentum and increasing adoption all over the world. TypeScript is, in the words of its own creator, a JavaScript that scales.

Tip

However, if you want to deep dive into the language and its possibilities, take a look at the excellent "Mastering TypeScript" by Nathan Rozentals, available at https://www.packtpub.com/web-development/mastering-typescript.

Let's remind ourselves that the project started around 2010 as a response to the growing popularity of JavaScript—not only in the browsers, but also on the servers. This means writing applications with not just hundreds of thousands, but sometimes millions of lines of code. The language itself lacks some features that we're accustomed to in large-scale application developments.

As we mentioned earlier, until ECMAScript 2015, we didn't have classes or modules or any static type system. This static type system is exactly what empowers tools such as V. Studio, Eclipse, JetBrains, and others to enable those features that we're used to in the development cycle.

Debugging TypeScript

Thanks to that static type system, TypeScript offers developers experiences parallel to those we would have using the C# language, and that includes debugging, as well.

As for debugging, TypeScript does not present any extra configuration or difficulties. Since it transpiles to plain JavaScript, all typical resources for JavaScript are usable here as well.

This is especially useful when using the embedded debugger of Visual Studio because you can set breakpoints in the TypeScript code as well (not only in JavaScript) and debug, as always, watching values at runtime and inspecting elements involved in the process.

For instance, in the previous code that we used in Chapter 4, Comparing Approaches for Programming, we can mark a breakpoint in the sorted.map call and watch the values of every element in the array, check the value of this, have access to the Globals definition, and—in general—witness all the goodness we would expect in a complete, extended, debugging session.

Just remember that you have to use Internet Explorer (the default browser for Visual Studio).

You can also use Edge as the default browser if you attach the Visual Studio debugger to the process in this manner using the following steps:

  1. Launch the execution and go to the Visual Studio Debugger menu.
  2. Enter the Attach to Process option, and in the dialog box, select the Attach to option to mark Script code.
  3. Finally, in the list of processes, select the MicrosoftEdgeCP.exe process in the list and mark a breakpoint.
  4. When you reload the page, the execution will stop at the breakpoint.

In addition, you can use Chrome to debug TypeScript code inside as well!

Debugging TypeScript with Chrome

Just open the previous code using Chrome as the navigator of your choice. Then, press F12, and you will get access to the Sources tab. From there, select the app.ts file and mark any line with a breakpoint.

When you reload the page, you will discover how the code stops in the TypeScript line you marked, and all variables and objects implied in the execution are perfectly available. The next screenshot illustrates this excellent feature:

Debugging TypeScript with Chrome

Tip

Note that Firefox doesn't support the insertAdjacentElement method. You should use appendChild instead.

Interfaces and strong typing

Let's think of a more complex object similar to a C# object, containing a field, methods with more than one signature (overloaded), and so on.

For example, we can declare a Calculator interface with the following definition:

interface Calculator {
  increment?: number,
  clear(): void,
  result(): number,
  add(n: number): void,
  add(): void,
  new (s: string): Element;
}

The notion of state is provided with an optional increment field (the same syntax as in C#), and four methods are defined. The first two are standard declarations, but the other two deserve a review.

The add method is overloaded. We have two definitions: one that gets a number and another with no arguments (both return void). When using an object that implements the Calculator interface, we'll discover that the editor recognizes overloading just as we would expect from a similar object programmed in C# (refer to the next figure):

Interfaces and strong typing

Finally, the new method is the way we define a constructor inside an interface. This constructor receives a string but returns Element. Element is, in turn, defined as an interface that represents an object in a document (https://developer.mozilla.org/en-US/docs/Web/API/Element). It's something that belongs to the DOM (Document Object Model); so, with TypeScript, we can manage almost any DOM component, just like we could in plain old JavaScript.

Implementing namespaces

Most evolved languages allow the concept of namespace. A namespace permits the developer to create areas of code that are totally separated from each other, avoiding the collision of member's names and functionalities.

TypeScript includes this concept using the module keyword. A module is a fragment of JavaScript whose members are private to the module. That is, they're not available outside the module unless we declare them in an explicit manner. This is something we do using the export keyword.

So, a module is declared using a simple, intuitive syntax:

module Utilities {
  // export generates a "closure" in JS 
  export class Tracker2 {
    count = 0;
    start() {
      // Something starts here...
      // Check the generated JS
    }
  }
}

Later, the module's exported members are accessible using the dot notation:

var t = new Utilities.Tracker2();
t.start();

Module declarations also admit several levels of indentation to clearly separate different areas of code:

module Acme.core.Utilities {
  export var x: number = 7;
  export class Tracker2 {
    count = 0;
    start() {
      // Something here 
    }
  }
}
// This requires "nested access"
Acme.core.Utilities.x;

To simplify access to nested modules, we can also define an alias with the import keyword, which is especially useful when areas tend to grow:

// Use of the "Import" technique
import ACU = Acme.core.Utilities;
ACU.x;
var h = new ACU.Tracker2();

Declarations, scope, and Intellisense

We must not assume that objects created by the "context" (the browser or the user agent) are accessed in TypeScript by default. For example, the document object that a navigator creates to represent the DOM is not strictly part of the language.

However, it's very easy to make these members accessible simply by declaring them using the declare keyword. Also, for this case, the TypeScript compiler automatically supplies a declaration because by default, it includes a 'lib.d.ts' file, which provides interface declarations for the built-in JavaScript library as well as the Document Object Model.

As the official documentation says, if you want additional help for other libraries, all you have to do is declare them, and the corresponding .ts library will be used. Imagine that we want to change the title of the document; according to the earlier code, we should write the following:

declare var document: Document;
document.title = "Hello";  // Ok because document has been declared

If we need support for jQuery, to mention a popular library, all we have to do is declare it in this way:

declare var $;

From this point, any reference to the $ symbol will offer the expected Intellisense in the editor provided that the description file for this library has been referenced.

Scope and encapsulation

Other important concepts in relation to the scope and visibility of members are the namespace and module declarations. A namespace allows the programmer to declare private members to a named module, making them invisible for the code not being included inside; so, they're similar to the concept of namespace that we've already seen and that is typical in .NET programming.

If we want to expose any member of a namespace, the exports keyword allows such definition so that we can have a partially exposed namespace with private members. For instance, take a look at the following code:

namespace myNS {
  var insideStr = "Inside a Module";
  export function greeter() {
    return insideStr;
  }
}
myNS.greeter();
myNS.insideStr;

If we check this code inside Visual Studio, we'll see an advice from the compiler as we pass over the last sentence, indicating that property insideStr doesn't exist inside the MyNS module (which really means that from a namespace perspective, this member is not declared accessible or maybe it doesn't exist).

On the other hand, no advice is given in reference to the exposed greeter method, since the exports clause was used in its declaration (for other OOP languages, we would say that the greeter member is public).

Scope and encapsulation

Classes and class inheritance

As we've seen, classes are a key part of TypeScript, and their declaration syntax is almost identical to the C# declarations we all know.

That is, we can declare private members, customized constructors, methods, access properties, and even static members so that they can be accessed using the name of the class instead of a variable instance. Take a look at this code written by Anders Hejlsberg in an online demo and published by Channel 9 (https://channel9.msdn.com/posts/Anders-Hejlsberg-Introducing-TypeScript, in case you want to follow all the details and comments that this author provides):

class PointWithColor {
  x: number;
  y: number;
  // Private members 
  private color: string;
  constructor(x: number, y: number) {
    this.x = x;
    this.y = y;
    this.color = "Blue"; // Intellisense -in- the class
  }
  distance() {
    return Math.sqrt(this.x * this.x + this.y * this.y);
  }
  // We can also (ES5) turn distance into a property
  get distanceP() {
    return Math.sqrt(this.x * this.x + this.y * this.y);
  }
  // And declare static members
  static origin = new PointWithColor(0, 0)
}

As you can see, there's a color declaration using the private keyword, a customized constructor, a read-only property (get distanceP), and a static declaration (origin) to establish the initial point of drawing.

Optionally, there is a variant of the { get; set; } construction of C# in the class' constructor, which allows you to simplify declarations and assign an initial value to the constructor's arguments.

With this syntax, we can write a simplified variation of the previous class, as follows:

class PointWithColor2 {
  // Private members 
  private color: string;
  // The following declaration produces the same effect 
  // as the previous as for accessibility of its members 
  // Even, assigning default values
  constructor(public x: number = 0, public y: number = 0) {
    this.color = "Red"; 
  }
  distance() { return Math.sqrt(this.x * this.x + this.y * this.y); }
  get distanceP() { return Math.sqrt(this.x * this.x + this.y * this.y); }
  static origen = new PointWithColor(0, 0)
}

Of course, in order to properly implement OOP, we also need inheritance. Inheritance is achieved using the extends keyword in the declaration of a class. So, we can define a new, inherited version of the previous class as follows:

class PointWithColor3D extends PointWithColor {
  // It uses the base class constructor, otherwise
  // creates a customized one.
  constructor(x: number, y: number, public z: number) {
    super(x, y);
  }
  // Method overloading 
  distance() {
    var d = super.distance();
    return Math.sqrt(d * d + this.z * this.z);
  }
}

The previous code uses another specific keyword here, super, in order to refer to the parent class. There's much more to the language, and we recommend the detailed documentation found at GitHub (https://github.com/Microsoft/TypeScript/blob/master/doc/spec.md) for more details and code snippets.

Functions

In the Greeter class discussed in the initial demo, the start() and stop() methods didn't have any return value. You can express a return value for a function in exactly the same way as we do with parameters: appending a colon (:) at the end, thus allowing us to express the whole signature of any function. So, for the typical add function, we can write the following:

add(x: number, y: number): number {
    return x + y;
}

One the most common practices in the language is to use interfaces to declare user-defined object types. Once declared, the interface will be checked against any member proclaiming its implementation:

interface Person {
    name: string, 
    age?: number  // the age is optional
}

function add(person: Person) {
    var name = person.name; // Ok
}

add({ name: "Peter" });  // Ok
add({ age: 37 });  // Error, name required
add({ name: "Irene", age: 17 });  // Ok

As you can see, age is declared with the same syntax as what we used with C# for optional values, except that no default value is required.

Similarly, we can declare a type and assign it a value in the same sentence:

// Declare and initialize
var person: {
    name: string;
    age: number;
    emails: string[];
} = {
        name: 'John',
        age: 5,
        emails: ['[email protected]', '[email protected]']
    }

If we declare a function using the lambda expression as one of the argument's syntaxes, the compiler infers that the type of the argument is a function:

function funcWithLambda(x: () => string) {
    x(); // Intellisense
}

This is how it is displayed in the IDE:

Functions

An interface may allow you also to declare method overloading. Take a look at this declaration and note the double definition for the doSomething method:

interface Thing {
    a: number;
    b: string;
    doSomething(s: string, n?: number): string; //methods
    // Method overloading
    doSomething(n: number): number;
}

function process(x: Thing) {
    x.doSomething("hola"); // Intellisense
    x.doSomething(3); // Ok
}

A variant of the previous declaration allows us to declare overloading and include a data field for the doSomething member:

// Methods with properties
// Equivalent to the previous + data
interface Thing3 {
    a: number;
    b: string;
    // Here we add a field data to doSomething
    doSomething: {
        (s: string): string;
        (n: number): number;
        data: any;
    };
}

Later, we can refer Thing3 using the following syntax:

function callThing3(x: Thing3) {
    x.doSomething("hello"); // Intellisense (overloading)
    x.doSomething(3);
    x.doSomething.data; // method with properties
}

Here, you can see how the three different references to the overloaded forms of doSomething are considered valid by the compiler. We even have the possibility of declaring constructors and indexers (much like in C#):

interface Thing4 {
    // Constructor
    new (s: string): Element;
    // Indexer
    [index: number]: Date;
}

function callThing4(x: Thing4) {
    // return new x("abc").getAttribute() -> returns Element
    return x[0].getDay(); // Date info
}

Another possibility is based on TypeScript's ability of defining an interface to enforce the return type:

interface Counter {
    delete(): void;
    add(x: number): void;
    result(): number;
}

function createCounter(): Counter {
    var total = 0;
    return {
        delete: function () { total = 0 },
        add: function (value: number) {
            total += value;
        },
        result: function () { return total; }
    };
}

var a = createCounter();
a.add(5); //Ok

// It's also useful for event handlers
window.onmousemove = function (e) {
    var pos = e.clientX; // Intellisense in the event
    // You can use the "Go to Definition" option ->
    // which takes you to "lib.d.ts" library
}

I want you to check the definition of any member, remember that right-clicking and selecting "Go to Definition" will open the corresponding lib.d.ts file and show the original definition of any member; for example, the clientX member of the event object will show the following information:

Functions

In the same way, we can import declarations from other libraries and use this technique to check those implementations: this includes jQuery, Bootstrap, and so on. The site Definitely Typed (https://github.com/DefinitelyTyped/DefinitelyTyped) holds hundreds of these definitions.

Moreover, there is still another way to declare overloaded functions: you can declare several signature methods and finish the block with a real function definition and implementation. This is done in order to avoid TypeScript from showing errors at compile time, although the final implementation in JavaScript will only consist of one function given that JavaScript doesn't have types.

In this way, the previous definitions are taken as overloaded versions of the last definition, such as what is shown in the next piece of code:

class OverloadedClass {
    overloadedMethod(aString: string): void;
    overloadedMethod(aNumber: number, aString: string): void;
    overloadedMethod(aStringOrANumber: any, aString?: string): void {
        // Version checking is performed on the first argument
        if (aStringOrANumber && typeof aStringOrANumber == "number")
            alert("Second version: aNumber = " + aStringOrANumber +
                ", aString = " + aString);
        else
            alert("First version: aString = " + aStringOrANumber);
    }
}

Arrays and interfaces

We can also use the concept of interface to declare conditions, types, and behaviors related to array elements.

Take a look at this code in which we enforce type and behavior for an array of mountains:

interface Mountain {
    name: string;
    height: number;
}
// Mountain interface declared
var mountains: Mountain[] = [];
// Every added element is checked
mountains.push({
    name: 'Pico de Orizaba',
    height: 5636,
});
mountains.push({
    name: 'Denali',
    height: 6190
});
mountains.push({
    name: 'Mount Logan',
    height: 5956
});

function compareHeights(a: Mountain, b: Mountain) {
    if (a.height > b.height) {
        return -1;
    }
    if (a.height < b.height) {
        return 1;
    }
    return 0;
}
// Array.sort method expects a comparer which takes 2 arguments
var mountainsByHeight = mountains.sort(compareHeights);
// Read the first element of the array (Highest)
var highestMoutain = mountainsByHeight[0];
console.log(highestMoutain.name); // Denali

The Mountain interface makes sure that every element belonging to the mountains array actually implements the Mountain definition so that it can be compared later, which is something you can check if you include this code in an HTML script section. In the Console output, the "Denali" mountain is correctly sorted to be the highest by the sort method of the array.

More TypeScript in action

So, let's take a look at some more TypeScript in action, starting with some other simple code. After creating an empty solution in Visual Studio and adding a JavaScript file (a .js extension), here, I'm using a code pattern offered in several demos on the official site in order to illustrate some changes these tools can offer. So, I type the following (a short function to sort an array and return the results):

// JavaScript source code
function sortByName(arg) {
  var result = arg.slice(0);
  result.sort(function (x, y) {
    return x.name.localCompare(y.name);
  });
  return result;
};

When we pass the mouse over the arg argument, the editor is unable to tell anything about the type of argument (with only this code, it is impossible to know anything else). If we write sortByName a couple of lines after the function, the editor recognizes the name, but it can't add any more information about it:

More TypeScript in action

Now, let's add a new file of the same name and copy the contents of the previous file inside, only changing the extension to .ts (TypeScript). Even with exactly the same content, the editor's behavior changes.

First, when you now pass the cursor over the argument, it says that it's of type any. This happens when you pass over the sortByName declaration outside the function as well.

However, it can get even better. Since the function expects you to operate with an array of a type that has a name and age property, we can declare that object as an interface which includes both properties, such as a Person interface (or any other interface that complies with this requirement). Now, we can explicitly define that arg is an array of type Person, indicating it in the argument declaration after a colon, so we have the following:

interface Person {
  name: string, 
  age: number
}
function sortByName(arg: Person[]) {}

And here, the magic starts to happen. When I pass the cursor over the argument, it now indicates the type it should receive, but also, if I hover over the arg.slice(0) code fragment, it gives me a detailed explanation of what it expects to receive, and when I move my cursor down, I see that there's a red squiggle under the localCompare method call, signifying that such a method doesn't exist on type string (because it recognizes that name is of the type defined previously).

You can see both hints in the following (compound) screenshot:

More TypeScript in action

So, there's a bunch of extra information available just by making a few changes in the code in order to instruct TypeScript about the types we're dealing with. We see this if we try to rewrite the name.local… call in the search for help.

If we do this, and retype the sentence, when we press the dot symbol next to return.name, we'll be given a list of possible values that the name property admits, including the correct form of writing the sentence that was misspelled, as shown in the next screenshot. We also see extra information about the parameters that localeCompare should receive and the number of overloads that it defines:

More TypeScript in action

Actually, since TypeScript supports advanced features, you can use them right now with total backward compatibility: for instance, we can change the function argument we pass to the sort method into a lambda expression, just as if we were using ECMAScript 2015.

Let's take a look at the entire example. We'll define an array of hardware appliances along with their prices and identification numbers. The target is to sort the array by name and dynamically generate an output in a page, showing the names and prices in the sorted array.

This is the code we'll use:

interface Entity {
  name: string,
  price: number, 
  id : number
}

var hardware: Entity[] = [
  { name: "Mouse", price: 9.95, id: 3 },
  { name: "Keyboard", price: 27.95, id: 1 },
  { name: "Printer", price: 49.95, id: 2 },
  { name: "Hard Drive", price: 72.95, id: 4 },
];

function sortByName(a: Entity[]) {
  var result = a.slice(0);

  result.sort(function (x, y) {
    return x.name.localeCompare(y.name);
  });
  return result;
}
window.onload = () => {
  var sorted = sortByName(hardware);
  sorted.map((e) => {
    var elem = document.createElement("p");
    document.body.insertAdjacentElement("beforeEnd", elem);
    elem.innerText = e.name.toString() + " - " + e.price.toString();
  });
}

First, the Entity declaration guarantees that the later array definition of type Entity[] is recognized by the editor. At the time of putting everything together, the window.onload event uses a lambda expression with no arguments.

In the body of this expression, a sorted array is produced from the original definition, and then the new map method included in JavaScript 5 is called, allowing us to pass a callback function to be executed for every element in the array.

Again, we use a lambda expression to define the callback function, where e will represent—sequentially—the elements of the array (entities). We will have Intellisense in the edition even when using the properties of e so that we ensure that all members end up correctly spelled.

The execution shows the list of elements, ordered by name, including the name and price fields, just as we expected:

More TypeScript in action

The DOM connection

The "DOM connection" we mentioned earlier is very helpful in a variety of cases. Imagine that we want an alert dialog indicating the X coordinate of the mouse when the cursor moves over the window. We could program something like this:

window.onmousemove = function (e) {
  alert("Mouse moved at X coord: " + e.clientX);
};

If we pass the mouse over the e argument (representing the event object), we'll be shown a tooltip containing the event definition as well. And if we write e. (dot)…, Intellisense will again show up knowing exactly what can be done with that object. (Refer to the following figure):

The DOM connection

Where does this extra Intellisense come from? We have the ability to check this feature in the same manner as we would in other languages in Visual Studio. Just mark onmousemove or the e object, and in the contextual menu, select Go to Definition or Peek definition.

The IDE opens a new window pointing to the definition extracted from a file called Lib.d.ts and shows every detail. As mentioned previously, this file is the declaration file for the entire DOM and all the JavaScript's standard runtime libraries (it contains about 8,000 lines of code declarations).

Also, anyone can write these declaration files and upload them to the DefinitelyTyped site, since its completely open source:

The DOM connection

So, let's summarize some of the most important points discussed up until this point:

  • We count on a formalization of the JavaScript types that allow excellent edition tools
  • We find type inference and structural typing since the very beginning
  • It all works with existing JavaScript, with no modifications
  • Once the code is compiled, everything goes away, and the resulting code is nothing but plain JavaScript of the version of your choice
..................Content has been hidden....................

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