Taking advantage of static typing

Static typing is what can provide better tooling for our development process. While writing JavaScript, the most that IDEs and text editors can do is syntax highlight and provide some basic autocompletion suggestions based on sophisticated static analysis of our code. This means that we can only verify that we haven't made any typos by running the code.

In the previous sections, we described only the new features provided by ECMAScript expected to be implemented by browsers in the near future. In this section, we will take a look at what TypeScript provides in order to help us be less error-prone and more productive. At the time of writing this book, there were no plans to implement built-in support for static typing in the browsers.

The TypeScript code goes through intermediate preprocessing that performs the type checking and drops all the type annotations in order to provide valid JavaScript supported by modern browsers.

Using explicit type definitions

Just like Java and C++, TypeScript allows us to explicitly declare the type of the given variable:

let foo: number = 42; 

The preceding line defines the variable foo in the current block using the let syntax. We explicitly declare that we want foo to be of the type number and we set the value of foo to 42.

Now, let's try to change the value of foo:

let foo: number = 42; 
foo = '42'; 

Here, after the declaration of foo, we will set its value to the string '42'. This is a perfectly valid JavaScript code; however, if we compile it using the TypeScript's compiler, we will get:

$ tsc basic.ts 
basic.ts(2,1): error TS2322: Type 'string' is not assignable to type 'number'.

Once foo is associated with the given type, we cannot assign it values belonging to different types. This is one of the reasons we can skip the explicit type definition in case we assign a value to the given variable:

let foo = 42; 
foo = '42'; 

The semantics of this code will be the same as the one with the explicit type definition because of the type inference of TypeScript. We'll further take a look at it at the end of this chapter.

The type any

All the types in TypeScript are subtypes of a type called any. We can declare variables belonging to the any type using the any keyword. Such variables can hold the value of any type:

let foo: any; 
foo = {}; 
foo = 'bar '; 
foo += 42; 
console.log(foo); // "bar 42" 

The preceding code is a valid TypeScript, and it will not throw any error during compilation or runtime. If we use the type any for all of our variables, we will be basically writing the code with dynamic typing, which drops all the benefits of the TypeScript's compiler. That's why we have to be careful with any and use it only when it is necessary.

All the other types in TypeScript belong to one of the following categories:

  • Primitive types: These include Number, String, Boolean, Void, Null, Undefined, and Enum types.
  • Union types: These types are out of the scope of this book. You can take a look at them in the specification of TypeScript.
  • Object types: These include Function types, classes and interface type references, array types, tuple types, function types, and constructor types.
  • Type parameters: These include Generics that will be described in the Writing generic code by using type parameters section.

Understanding the Primitive types

Most of the primitive types in TypeScript are the ones we are already familiar with in JavaScript: Number, String, Boolean, Null, and Undefined. So, we will skip their formal explanation here. Another set of types that is handy while developing Angular applications is the Enum types defined by users.

The Enum types

The Enum types are primitive user-defined types that, according to the specification, are subclasses of Number. The concept of enums exists in the Java, C++, and C# languages, and it has the same semantics in TypeScript-user-defined types consisting of sets of named values called elements. In TypeScript, we can define an enum using the following syntax:

enum STATES { 
  CONNECTING, 
  CONNECTED, 
  DISCONNECTING, 
  WAITING, 
  DISCONNECTED  
}; 

This will be translated to the following JavaScript:

var STATES; 
(function (STATES) { 
    STATES[STATES["CONNECTING"] = 0] = "CONNECTING"; 
    STATES[STATES["CONNECTED"] = 1] = "CONNECTED"; 
    STATES[STATES["DISCONNECTING"] = 2] = "DISCONNECTING"; 
    STATES[STATES["WAITING"] = 3] = "WAITING"; 
    STATES[STATES["DISCONNECTED"] = 4] = "DISCONNECTED"; 
})(STATES || (STATES = {})); 

We can use the enum type as follows:

if (this.state === STATES.CONNECTING) { 
  console.log('The system is connecting'); 
} 

Understanding the Object types

In this section, we will take a look at the Array types and Function types, which belong to the more generic class of Object types. We will also explore how we can define classes and interfaces. Tuple types were introduced by TypeScript 1.3, and their main purpose is to allow the language to begin typing the new features introduced by ES2015, such as destructuring. We will not describe them in this book. For further reading, you can take a look at the language's specification at http://www.typescriptlang.org.

The Array types

In TypeScript, arrays are JavaScript arrays with a common element type. This means that we cannot have elements from different types in a given array. We have different array types for all the built-in types in TypeScript, plus all the custom types that we define.

We can define an array of numbers as follows:

let primes: number[] = []; 
primes.push(2); 
primes.push(3); 

If we want to have an array, which seems heterogeneous, similar to the arrays in JavaScript, we can use the type reference to any:

let randomItems: any[] = []; 
randomItems.push(1); 
randomItems.push('foo'); 
randomItems.push([]); 
randomItems.push({}); 

This is possible since the types of all the values we're pushing to the array are subtypes of the any type and the array we've declared contains values of the type any.

We can use the array methods we're familiar with in JavaScript with all the TypeScript Array types:

let randomItems: any[] = []; 
randomItems.push('foo'); 
randomItems.push('bar'); 
randomItems.join(''); // foobar 
randomItems.splice(1, 0, 'baz'); 
randomItems.join(''); // foobazbar 

We also have the square-brackets operator that gives us random access to the array's elements:

let randomItems: any[] = []; 
randomItems.push('foo'); 
randomItems.push('bar'); 
randomItems[0] === 'foo' 
randomItems[1] === 'bar' 

The Function types

We're already familiar with how to define a new function in JavaScript. We can use function expression or function declaration:

// function expression 
var isPrime = function (n) { 
  // body 
};
 
// function declaration 
function isPrime(n) { 
  // body 
}; 

Alternatively, we can use the new arrow function syntax:

var isPrime = n => { 
  // body 
}; 

The only thing TypeScript alters is the feature to define the types of the function's arguments and the type of its return result (i.e. the function's signature). After the compiler of the language performs its type checking and transpilation, all the type annotations will be removed. If we use function expression and assign a function to a variable, we will be able to define the variable type in the following way:

let variable: (arg1: type1, arg2: type2, ..., argn: typen) => returnType 

Consider the following example:

let isPrime: (n: number) => boolean = n => { 
  // body 
}; 

If we want to define a method in an object literal, we can do it in the following way:

let math = { 
  squareRoot(n: number): number { 
    // ... 
  } 
}; 

In the preceding example, we defined an object literal with method called squareRoot using the ES2015 syntax.

In case we want to define a function that produces some side effects instead of returning a result, we can declare it's return type as void:

let person = { 
  _name: null, 
  setName(name: string): void { 
    this._name = name; 
  } 
}; 

Defining classes

TypeScript classes are similar to what ES2015 offers. However, it alters the type declarations and adds more syntax sugar. For example, let's take the Human class we defined earlier and make it a valid TypeScript class:

class Human { 
  static totalPeople = 0; 
  _name: string;
 
  constructor(name) { 
    this._name = name; 
    Human.totalPeople += 1; 
  }
 
  get name() { 
    return this._name; 
  }
 
  set name(val) { 
    this._name = val; 
  }
 
  talk() { 
    return `Hi, I'm ${this.name}!`; 
  } 
} 

There is no difference between the current TypeScript definition with the one we already introduced; however, in this case, the declaration of the _name property is mandatory. Here is how we can use the class:

let human = new Human('foo'); 
console.log(human._name); 

Using access modifiers

Similarly, for most conventional object-oriented languages that support classes, TypeScript allows a definition of access modifiers. In order to deny direct access to the _name property outside the class it is defined in, we can declare it as private:

class Human { 
  static totalPeople = 0; 
  private _name: string; 
  // ... 
} 

The supported access modifiers by TypeScript are as follows:

  • Public: All the properties and methods declared as public can be accessed from anywhere.
  • Private: All the properties and methods declared as private can be accessed only from inside the class' definition itself.
  • Protected: All the properties and methods declared as protected can be accessed from inside the class' definition or the definition of any other class extending the one that owns the property or the method.

Access modifiers are a great way to implement Angular services with good encapsulation and a well-defined interface. In order to understand it better, let's take a look at an example using the class' hierarchy defined earlier, which is ported to TypeScript:

class Human { 
  static totalPeople = 0;
 
  constructor(protected name: string, private age: number) { 
    Human.totalPeople += 1; 
  }
 
  talk() { 
    return `Hi, I'm ${this.name}!`; 
  } 
} 
 
class Developer extends Human { 
  constructor(name: string, private languages: string[], age: number) { 
    super(name, age); 
  }
 
  talk() { 
    return `${super.talk()} And I know ${this.languages.join(', ')}.`; 
  } 
} 

Just like ES2015, TypeScript supports the extends keyword and desugars it to the prototypal JavaScript inheritance.

In the preceding example, we set the access modifiers of the name and age properties directly inside the constructor function. The semantics behind this syntax differs from the one used in the previous example. It has the following meaning: define a protected property called name of the type string and assign the first value passed to the constructor call to it. It is the same for the private age property. This saves us from explicitly setting the value in the constructor itself. If we take a look at the constructor of the Developer class, we can see that we can use the mixture between these syntaxes. We can explicitly define the property in the constructor's signature, or we can only define that the constructor accepts a parameters of the given types.

Now, let's create a new instance of the Developer class:

let dev = new Developer('foo', ['JavaScript', 'Go'], 42); 
dev.languages = ['Java']; 

During compilation, TypeScript will throw an error telling us that: Property languages is private and only accessible inside the class "Developer". Now, let's see what will happen if we create a new Human class and try to access its properties from outside its definition:

let human = new Human('foo', 42); 
human.age = 42; 
human.name = 'bar'; 

In this case, we'll get the following two errors:

Property age is private and is only accessible inside the class "Human", and the Property name is a protected and only accessible inside class "Human" and its subclasses.

However, if we try to access the _name property from inside the definition of Developer, the compiler won't throw any errors.

In order to get a better sense of what the TypeScript compiler will produce out of a type-annotated class, let's take a look at the JavaScript produced by the following definition:

class Human { 
  constructor(private name: string) {} 
} 

The resulting ECMAScript 5 will be as follows:

var Human = (function () { 
    function Human(name) { 
        this.name = name; 
    } 
    return Human; 
})(); 

The defined property is added directly to the objects instantiated by calling the constructor function with the operator new. This means that once the code is compiled, we can directly access the private members of the created objects. In order to wrap this up, access modifiers are added in the language in order to help us enforce better encapsulation and get compile-time errors in case we violate it.

Defining interfaces

Subtyping in programming languages allows us to treat objects in the same way based on the observation that they are specialized versions of a generic object. This doesn't mean that they have to be instances of the same class of objects, or that they have a complete intersection between their interfaces. The objects might have only a few common properties and still be treated the same way in a specific context. In JavaScript, we usually use duck typing. We may invoke specific methods for all the objects passed to a function based on the assumption that these methods exist. However, all of us have experienced the undefined is not a function error thrown by the JavaScript interpreter.

Object-oriented programming and TypeScript come with a solution. They allow us to make sure that our objects have similar behavior if they implement interfaces that declare the subset of the properties they own.

For example, we can define our interface Accountable:

interface Accountable { 
  getIncome(): number; 
} 

Now, we can make sure that both Individual and Firm implement this interface by performing the following:

class Firm implements Accountable { 
  getIncome(): number { 
    // ... 
  } 
}
 
class Individual implements Accountable { 
  getIncome(): number { 
    // ... 
  } 
} 

In case we implement a given interface, we need to provide an implementation for all the methods defined inside it, otherwise, the TypeScript compiler will throw an error. The methods we implement must have the same signature as the ones declared in the interface definition.

TypeScript interfaces also support properties. In the Accountable interface, we can include a field called accountNumber with a type of string:

interface Accountable { 
  accountNumber: string; 
  getIncome(): number; 
} 

We can define it in our class as a field or a getter.

Interface inheritance

Interfaces may also extend each other. For example, we may turn our Individual class into an interface that has a social security number:

interface Accountable { 
  accountNumber: string; 
  getIncome(): number; 
}
 
interface Individual extends Accountable { 
  ssn: string; 
} 

Since interfaces support multiple inheritances, Individual may also extend the interface Human that has the name and age properties:

interface Accountable { 
  accountNumber: string; 
  getIncome(): number; 
}
 
interface Human { 
  age: number; 
  name: number; 
}
 
interface Individual extends Accountable, Human { 
  ssn: string; 
} 

Implementing multiple interfaces

In case the class's behavior is a union of the properties defined in a couple of interfaces, it may implement all of them:

class Person implements Human, Accountable { 
  age: number; 
  name: string; 
  accountNumber: string;
 
  getIncome(): number { 
    // ... 
  } 
} 

In this case, we need to provide the implementation of all the methods declared inside the interfaces our class implements, otherwise, the compiler will throw a compile-time error.

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

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